Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <LibWeb/CSS/SelectorEngine.h>
28#include <LibWeb/CSS/StyleResolver.h>
29#include <LibWeb/CSS/StyleSheet.h>
30#include <LibWeb/DOM/Document.h>
31#include <LibWeb/DOM/Element.h>
32#include <LibWeb/Dump.h>
33#include <LibWeb/Parser/CSSParser.h>
34#include <ctype.h>
35#include <stdio.h>
36
37namespace Web {
38
39StyleResolver::StyleResolver(Document& document)
40 : m_document(document)
41{
42}
43
44StyleResolver::~StyleResolver()
45{
46}
47
48static StyleSheet& default_stylesheet()
49{
50 static StyleSheet* sheet;
51 if (!sheet) {
52 extern const char default_stylesheet_source[];
53 String css = default_stylesheet_source;
54 sheet = parse_css(css).leak_ref();
55 }
56 return *sheet;
57}
58
59template<typename Callback>
60void StyleResolver::for_each_stylesheet(Callback callback) const
61{
62 callback(default_stylesheet());
63 for (auto& sheet : document().stylesheets()) {
64 callback(sheet);
65 }
66}
67
68NonnullRefPtrVector<StyleRule> StyleResolver::collect_matching_rules(const Element& element) const
69{
70 NonnullRefPtrVector<StyleRule> matching_rules;
71
72 for_each_stylesheet([&](auto& sheet) {
73 for (auto& rule : sheet.rules()) {
74 for (auto& selector : rule.selectors()) {
75 if (SelectorEngine::matches(selector, element)) {
76 matching_rules.append(rule);
77 break;
78 }
79 }
80 }
81 });
82
83#ifdef HTML_DEBUG
84 dbgprintf("Rules matching Element{%p}\n", &element);
85 for (auto& rule : matching_rules) {
86 dump_rule(rule);
87 }
88#endif
89
90 return matching_rules;
91}
92
93bool StyleResolver::is_inherited_property(CSS::PropertyID property_id)
94{
95 static HashTable<CSS::PropertyID> inherited_properties;
96 if (inherited_properties.is_empty()) {
97 inherited_properties.set(CSS::PropertyID::BorderCollapse);
98 inherited_properties.set(CSS::PropertyID::BorderSpacing);
99 inherited_properties.set(CSS::PropertyID::Color);
100 inherited_properties.set(CSS::PropertyID::FontFamily);
101 inherited_properties.set(CSS::PropertyID::FontSize);
102 inherited_properties.set(CSS::PropertyID::FontStyle);
103 inherited_properties.set(CSS::PropertyID::FontVariant);
104 inherited_properties.set(CSS::PropertyID::FontWeight);
105 inherited_properties.set(CSS::PropertyID::LetterSpacing);
106 inherited_properties.set(CSS::PropertyID::LineHeight);
107 inherited_properties.set(CSS::PropertyID::ListStyle);
108 inherited_properties.set(CSS::PropertyID::ListStyleImage);
109 inherited_properties.set(CSS::PropertyID::ListStylePosition);
110 inherited_properties.set(CSS::PropertyID::ListStyleType);
111 inherited_properties.set(CSS::PropertyID::TextAlign);
112 inherited_properties.set(CSS::PropertyID::TextIndent);
113 inherited_properties.set(CSS::PropertyID::TextTransform);
114 inherited_properties.set(CSS::PropertyID::Visibility);
115 inherited_properties.set(CSS::PropertyID::WhiteSpace);
116 inherited_properties.set(CSS::PropertyID::WordSpacing);
117
118 // FIXME: This property is not supposed to be inherited, but we currently
119 // rely on inheritance to propagate decorations into line boxes.
120 inherited_properties.set(CSS::PropertyID::TextDecoration);
121 }
122 return inherited_properties.contains(property_id);
123}
124
125static Vector<String> split_on_whitespace(const StringView& string)
126{
127 if (string.is_empty())
128 return {};
129
130 Vector<String> v;
131 size_t substart = 0;
132 for (size_t i = 0; i < string.length(); ++i) {
133 char ch = string.characters_without_null_termination()[i];
134 if (isspace(ch)) {
135 size_t sublen = i - substart;
136 if (sublen != 0)
137 v.append(string.substring_view(substart, sublen));
138 substart = i + 1;
139 }
140 }
141 size_t taillen = string.length() - substart;
142 if (taillen != 0)
143 v.append(string.substring_view(substart, taillen));
144 return v;
145}
146
147static inline void set_property_border_width(StyleProperties& style, const StyleValue& value)
148{
149 ASSERT(value.is_length());
150 style.set_property(CSS::PropertyID::BorderTopWidth, value);
151 style.set_property(CSS::PropertyID::BorderRightWidth, value);
152 style.set_property(CSS::PropertyID::BorderBottomWidth, value);
153 style.set_property(CSS::PropertyID::BorderLeftWidth, value);
154}
155
156static inline void set_property_border_color(StyleProperties& style, const StyleValue& value)
157{
158 ASSERT(value.is_color());
159 style.set_property(CSS::PropertyID::BorderTopColor, value);
160 style.set_property(CSS::PropertyID::BorderRightColor, value);
161 style.set_property(CSS::PropertyID::BorderBottomColor, value);
162 style.set_property(CSS::PropertyID::BorderLeftColor, value);
163}
164
165static inline void set_property_border_style(StyleProperties& style, const StyleValue& value)
166{
167 ASSERT(value.is_string());
168 style.set_property(CSS::PropertyID::BorderTopStyle, value);
169 style.set_property(CSS::PropertyID::BorderRightStyle, value);
170 style.set_property(CSS::PropertyID::BorderBottomStyle, value);
171 style.set_property(CSS::PropertyID::BorderLeftStyle, value);
172}
173
174static void set_property_expanding_shorthands(StyleProperties& style, CSS::PropertyID property_id, const StyleValue& value)
175{
176 if (property_id == CSS::PropertyID::Border) {
177 auto parts = split_on_whitespace(value.to_string());
178 if (value.is_length()) {
179 set_property_border_width(style, value);
180 return;
181 }
182 if (value.is_color()) {
183 set_property_border_color(style, value);
184 return;
185 }
186 if (value.is_string()) {
187 auto parts = split_on_whitespace(value.to_string());
188
189 if (parts.size() == 1) {
190 if (auto value = parse_line_style(parts[0])) {
191 set_property_border_style(style, value.release_nonnull());
192 set_property_border_color(style, ColorStyleValue::create(Gfx::Color::Black));
193 set_property_border_width(style, LengthStyleValue::create(Length(3, Length::Type::Absolute)));
194 return;
195 }
196 }
197
198 RefPtr<LengthStyleValue> line_width_value;
199 RefPtr<ColorStyleValue> color_value;
200 RefPtr<StringStyleValue> line_style_value;
201
202 for (auto& part : parts) {
203 if (auto value = parse_line_width(part)) {
204 if (line_width_value)
205 return;
206 line_width_value = move(value);
207 continue;
208 }
209 if (auto value = parse_color(part)) {
210 if (color_value)
211 return;
212 color_value = move(value);
213 continue;
214 }
215 if (auto value = parse_line_style(part)) {
216 if (line_style_value)
217 return;
218 line_style_value = move(value);
219 continue;
220 }
221 }
222
223 if (line_width_value)
224 set_property_border_width(style, line_width_value.release_nonnull());
225 if (color_value)
226 set_property_border_color(style, color_value.release_nonnull());
227 if (line_style_value)
228 set_property_border_style(style, line_style_value.release_nonnull());
229
230 return;
231 }
232 return;
233 }
234
235 if (property_id == CSS::PropertyID::BorderStyle) {
236 style.set_property(CSS::PropertyID::BorderTopStyle, value);
237 style.set_property(CSS::PropertyID::BorderRightStyle, value);
238 style.set_property(CSS::PropertyID::BorderBottomStyle, value);
239 style.set_property(CSS::PropertyID::BorderLeftStyle, value);
240 return;
241 }
242
243 if (property_id == CSS::PropertyID::BorderWidth) {
244 style.set_property(CSS::PropertyID::BorderTopWidth, value);
245 style.set_property(CSS::PropertyID::BorderRightWidth, value);
246 style.set_property(CSS::PropertyID::BorderBottomWidth, value);
247 style.set_property(CSS::PropertyID::BorderLeftWidth, value);
248 return;
249 }
250
251 if (property_id == CSS::PropertyID::BorderColor) {
252 style.set_property(CSS::PropertyID::BorderTopColor, value);
253 style.set_property(CSS::PropertyID::BorderRightColor, value);
254 style.set_property(CSS::PropertyID::BorderBottomColor, value);
255 style.set_property(CSS::PropertyID::BorderLeftColor, value);
256 return;
257 }
258
259 if (property_id == CSS::PropertyID::Margin) {
260 if (value.is_length()) {
261 style.set_property(CSS::PropertyID::MarginTop, value);
262 style.set_property(CSS::PropertyID::MarginRight, value);
263 style.set_property(CSS::PropertyID::MarginBottom, value);
264 style.set_property(CSS::PropertyID::MarginLeft, value);
265 return;
266 }
267 if (value.is_string()) {
268 auto parts = split_on_whitespace(value.to_string());
269 if (parts.size() == 2) {
270 auto vertical = parse_css_value(parts[0]);
271 auto horizontal = parse_css_value(parts[1]);
272 style.set_property(CSS::PropertyID::MarginTop, vertical);
273 style.set_property(CSS::PropertyID::MarginBottom, vertical);
274 style.set_property(CSS::PropertyID::MarginLeft, horizontal);
275 style.set_property(CSS::PropertyID::MarginRight, horizontal);
276 return;
277 }
278 if (parts.size() == 3) {
279 auto top = parse_css_value(parts[0]);
280 auto horizontal = parse_css_value(parts[1]);
281 auto bottom = parse_css_value(parts[2]);
282 style.set_property(CSS::PropertyID::MarginTop, top);
283 style.set_property(CSS::PropertyID::MarginBottom, bottom);
284 style.set_property(CSS::PropertyID::MarginLeft, horizontal);
285 style.set_property(CSS::PropertyID::MarginRight, horizontal);
286 return;
287 }
288 if (parts.size() == 4) {
289 auto top = parse_css_value(parts[0]);
290 auto right = parse_css_value(parts[1]);
291 auto bottom = parse_css_value(parts[2]);
292 auto left = parse_css_value(parts[3]);
293 style.set_property(CSS::PropertyID::MarginTop, top);
294 style.set_property(CSS::PropertyID::MarginBottom, bottom);
295 style.set_property(CSS::PropertyID::MarginLeft, left);
296 style.set_property(CSS::PropertyID::MarginRight, right);
297 return;
298 }
299 dbg() << "Unsure what to do with CSS margin value '" << value.to_string() << "'";
300 return;
301 }
302 return;
303 }
304
305 if (property_id == CSS::PropertyID::Padding) {
306 if (value.is_length()) {
307 style.set_property(CSS::PropertyID::PaddingTop, value);
308 style.set_property(CSS::PropertyID::PaddingRight, value);
309 style.set_property(CSS::PropertyID::PaddingBottom, value);
310 style.set_property(CSS::PropertyID::PaddingLeft, value);
311 return;
312 }
313 if (value.is_string()) {
314 auto parts = split_on_whitespace(value.to_string());
315 if (parts.size() == 2) {
316 auto vertical = parse_css_value(parts[0]);
317 auto horizontal = parse_css_value(parts[1]);
318 style.set_property(CSS::PropertyID::PaddingTop, vertical);
319 style.set_property(CSS::PropertyID::PaddingBottom, vertical);
320 style.set_property(CSS::PropertyID::PaddingLeft, horizontal);
321 style.set_property(CSS::PropertyID::PaddingRight, horizontal);
322 return;
323 }
324 if (parts.size() == 3) {
325 auto top = parse_css_value(parts[0]);
326 auto horizontal = parse_css_value(parts[1]);
327 auto bottom = parse_css_value(parts[2]);
328 style.set_property(CSS::PropertyID::PaddingTop, top);
329 style.set_property(CSS::PropertyID::PaddingBottom, bottom);
330 style.set_property(CSS::PropertyID::PaddingLeft, horizontal);
331 style.set_property(CSS::PropertyID::PaddingRight, horizontal);
332 return;
333 }
334 if (parts.size() == 4) {
335 auto top = parse_css_value(parts[0]);
336 auto right = parse_css_value(parts[1]);
337 auto bottom = parse_css_value(parts[2]);
338 auto left = parse_css_value(parts[3]);
339 style.set_property(CSS::PropertyID::PaddingTop, top);
340 style.set_property(CSS::PropertyID::PaddingBottom, bottom);
341 style.set_property(CSS::PropertyID::PaddingLeft, left);
342 style.set_property(CSS::PropertyID::PaddingRight, right);
343 return;
344 }
345 dbg() << "Unsure what to do with CSS padding value '" << value.to_string() << "'";
346 return;
347 }
348 return;
349 }
350
351 style.set_property(property_id, value);
352}
353
354NonnullRefPtr<StyleProperties> StyleResolver::resolve_style(const Element& element, const StyleProperties* parent_style) const
355{
356 auto style = StyleProperties::create();
357
358 if (parent_style) {
359 parent_style->for_each_property([&](auto property_id, auto& value) {
360 if (is_inherited_property(property_id))
361 set_property_expanding_shorthands(style, property_id, value);
362 });
363 }
364
365 element.apply_presentational_hints(*style);
366
367 auto matching_rules = collect_matching_rules(element);
368 for (auto& rule : matching_rules) {
369 for (auto& property : rule.declaration().properties()) {
370 set_property_expanding_shorthands(style, property.property_id, property.value);
371 }
372 }
373
374 auto style_attribute = element.attribute("style");
375 if (!style_attribute.is_null()) {
376 if (auto declaration = parse_css_declaration(style_attribute)) {
377 for (auto& property : declaration->properties()) {
378 set_property_expanding_shorthands(style, property.property_id, property.value);
379 }
380 }
381 }
382
383 return style;
384}
385
386}