GradientParsing.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. /*
  2. * Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
  3. * Copyright (c) 2020-2021, the SerenityOS developers.
  4. * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
  5. * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
  6. * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
  7. *
  8. * SPDX-License-Identifier: BSD-2-Clause
  9. */
  10. #include <AK/Debug.h>
  11. #include <LibWeb/CSS/Parser/Parser.h>
  12. #include <LibWeb/CSS/StyleValues/ConicGradientStyleValue.h>
  13. #include <LibWeb/CSS/StyleValues/LinearGradientStyleValue.h>
  14. #include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
  15. #include <LibWeb/CSS/StyleValues/RadialGradientStyleValue.h>
  16. namespace Web::CSS::Parser {
  17. template<typename TElement>
  18. Optional<Vector<TElement>> Parser::parse_color_stop_list(TokenStream<ComponentValue>& tokens, auto is_position, auto get_position)
  19. {
  20. enum class ElementType {
  21. Garbage,
  22. ColorStop,
  23. ColorHint
  24. };
  25. auto parse_color_stop_list_element = [&](TElement& element) -> ElementType {
  26. tokens.discard_whitespace();
  27. if (!tokens.has_next_token())
  28. return ElementType::Garbage;
  29. RefPtr<CSSStyleValue> color;
  30. Optional<typename TElement::PositionType> position;
  31. Optional<typename TElement::PositionType> second_position;
  32. if (auto dimension = parse_dimension(tokens.next_token()); dimension.has_value() && is_position(*dimension)) {
  33. // [<T-percentage> <color>] or [<T-percentage>]
  34. position = get_position(*dimension);
  35. tokens.discard_a_token(); // dimension
  36. tokens.discard_whitespace();
  37. // <T-percentage>
  38. if (!tokens.has_next_token() || tokens.next_token().is(Token::Type::Comma)) {
  39. element.transition_hint = typename TElement::ColorHint { *position };
  40. return ElementType::ColorHint;
  41. }
  42. // <T-percentage> <color>
  43. auto maybe_color = parse_color_value(tokens);
  44. if (!maybe_color)
  45. return ElementType::Garbage;
  46. color = maybe_color.release_nonnull();
  47. } else {
  48. // [<color> <T-percentage>?]
  49. auto maybe_color = parse_color_value(tokens);
  50. if (!maybe_color)
  51. return ElementType::Garbage;
  52. color = maybe_color.release_nonnull();
  53. tokens.discard_whitespace();
  54. // Allow up to [<color> <T-percentage> <T-percentage>] (double-position color stops)
  55. // Note: Double-position color stops only appear to be valid in this order.
  56. for (auto stop_position : Array { &position, &second_position }) {
  57. if (tokens.has_next_token() && !tokens.next_token().is(Token::Type::Comma)) {
  58. auto dimension = parse_dimension(tokens.consume_a_token());
  59. if (!dimension.has_value() || !is_position(*dimension))
  60. return ElementType::Garbage;
  61. *stop_position = get_position(*dimension);
  62. tokens.discard_whitespace();
  63. }
  64. }
  65. }
  66. element.color_stop = typename TElement::ColorStop { color, position, second_position };
  67. return ElementType::ColorStop;
  68. };
  69. TElement first_element {};
  70. if (parse_color_stop_list_element(first_element) != ElementType::ColorStop)
  71. return {};
  72. if (!tokens.has_next_token())
  73. return {};
  74. Vector<TElement> color_stops { first_element };
  75. while (tokens.has_next_token()) {
  76. TElement list_element {};
  77. tokens.discard_whitespace();
  78. if (!tokens.consume_a_token().is(Token::Type::Comma))
  79. return {};
  80. auto element_type = parse_color_stop_list_element(list_element);
  81. if (element_type == ElementType::ColorHint) {
  82. // <color-hint>, <color-stop>
  83. tokens.discard_whitespace();
  84. if (!tokens.consume_a_token().is(Token::Type::Comma))
  85. return {};
  86. // Note: This fills in the color stop on the same list_element as the color hint (it does not overwrite it).
  87. if (parse_color_stop_list_element(list_element) != ElementType::ColorStop)
  88. return {};
  89. } else if (element_type == ElementType::ColorStop) {
  90. // <color-stop>
  91. } else {
  92. return {};
  93. }
  94. color_stops.append(list_element);
  95. }
  96. return color_stops;
  97. }
  98. static StringView consume_if_starts_with(StringView str, StringView start, auto found_callback)
  99. {
  100. if (str.starts_with(start, CaseSensitivity::CaseInsensitive)) {
  101. found_callback();
  102. return str.substring_view(start.length());
  103. }
  104. return str;
  105. }
  106. Optional<Vector<LinearColorStopListElement>> Parser::parse_linear_color_stop_list(TokenStream<ComponentValue>& tokens)
  107. {
  108. // <color-stop-list> =
  109. // <linear-color-stop> , [ <linear-color-hint>? , <linear-color-stop> ]#
  110. return parse_color_stop_list<LinearColorStopListElement>(
  111. tokens,
  112. [](Dimension& dimension) { return dimension.is_length_percentage(); },
  113. [](Dimension& dimension) { return dimension.length_percentage(); });
  114. }
  115. Optional<Vector<AngularColorStopListElement>> Parser::parse_angular_color_stop_list(TokenStream<ComponentValue>& tokens)
  116. {
  117. // <angular-color-stop-list> =
  118. // <angular-color-stop> , [ <angular-color-hint>? , <angular-color-stop> ]#
  119. return parse_color_stop_list<AngularColorStopListElement>(
  120. tokens,
  121. [](Dimension& dimension) { return dimension.is_angle_percentage(); },
  122. [](Dimension& dimension) { return dimension.angle_percentage(); });
  123. }
  124. RefPtr<CSSStyleValue> Parser::parse_linear_gradient_function(TokenStream<ComponentValue>& outer_tokens)
  125. {
  126. using GradientType = LinearGradientStyleValue::GradientType;
  127. auto transaction = outer_tokens.begin_transaction();
  128. auto& component_value = outer_tokens.consume_a_token();
  129. if (!component_value.is_function())
  130. return nullptr;
  131. GradientRepeating repeating_gradient = GradientRepeating::No;
  132. GradientType gradient_type { GradientType::Standard };
  133. auto function_name = component_value.function().name().bytes_as_string_view();
  134. function_name = consume_if_starts_with(function_name, "-webkit-"sv, [&] {
  135. gradient_type = GradientType::WebKit;
  136. });
  137. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  138. repeating_gradient = GradientRepeating::Yes;
  139. });
  140. if (!function_name.equals_ignoring_ascii_case("linear-gradient"sv))
  141. return nullptr;
  142. // linear-gradient() = linear-gradient([ <angle> | to <side-or-corner> ]?, <color-stop-list>)
  143. TokenStream tokens { component_value.function().values() };
  144. tokens.discard_whitespace();
  145. if (!tokens.has_next_token())
  146. return nullptr;
  147. bool has_direction_param = true;
  148. LinearGradientStyleValue::GradientDirection gradient_direction = gradient_type == GradientType::Standard
  149. ? SideOrCorner::Bottom
  150. : SideOrCorner::Top;
  151. auto to_side = [](StringView value) -> Optional<SideOrCorner> {
  152. if (value.equals_ignoring_ascii_case("top"sv))
  153. return SideOrCorner::Top;
  154. if (value.equals_ignoring_ascii_case("bottom"sv))
  155. return SideOrCorner::Bottom;
  156. if (value.equals_ignoring_ascii_case("left"sv))
  157. return SideOrCorner::Left;
  158. if (value.equals_ignoring_ascii_case("right"sv))
  159. return SideOrCorner::Right;
  160. return {};
  161. };
  162. auto is_to_side_or_corner = [&](auto const& token) {
  163. if (!token.is(Token::Type::Ident))
  164. return false;
  165. if (gradient_type == GradientType::WebKit)
  166. return to_side(token.token().ident()).has_value();
  167. return token.token().ident().equals_ignoring_ascii_case("to"sv);
  168. };
  169. auto const& first_param = tokens.next_token();
  170. if (first_param.is(Token::Type::Dimension)) {
  171. // <angle>
  172. tokens.discard_a_token();
  173. auto angle_value = first_param.token().dimension_value();
  174. auto unit_string = first_param.token().dimension_unit();
  175. auto angle_type = Angle::unit_from_name(unit_string);
  176. if (!angle_type.has_value())
  177. return nullptr;
  178. gradient_direction = Angle { angle_value, angle_type.release_value() };
  179. } else if (is_to_side_or_corner(first_param)) {
  180. // <side-or-corner> = [left | right] || [top | bottom]
  181. // Note: -webkit-linear-gradient does not include to the "to" prefix on the side or corner
  182. if (gradient_type == GradientType::Standard) {
  183. tokens.discard_a_token();
  184. tokens.discard_whitespace();
  185. if (!tokens.has_next_token())
  186. return nullptr;
  187. }
  188. // [left | right] || [top | bottom]
  189. auto const& first_side = tokens.consume_a_token();
  190. if (!first_side.is(Token::Type::Ident))
  191. return nullptr;
  192. auto side_a = to_side(first_side.token().ident());
  193. tokens.discard_whitespace();
  194. Optional<SideOrCorner> side_b;
  195. if (tokens.has_next_token() && tokens.next_token().is(Token::Type::Ident))
  196. side_b = to_side(tokens.consume_a_token().token().ident());
  197. if (side_a.has_value() && !side_b.has_value()) {
  198. gradient_direction = *side_a;
  199. } else if (side_a.has_value() && side_b.has_value()) {
  200. // Convert two sides to a corner
  201. if (to_underlying(*side_b) < to_underlying(*side_a))
  202. swap(side_a, side_b);
  203. if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Left)
  204. gradient_direction = SideOrCorner::TopLeft;
  205. else if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Right)
  206. gradient_direction = SideOrCorner::TopRight;
  207. else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Left)
  208. gradient_direction = SideOrCorner::BottomLeft;
  209. else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Right)
  210. gradient_direction = SideOrCorner::BottomRight;
  211. else
  212. return nullptr;
  213. } else {
  214. return nullptr;
  215. }
  216. } else {
  217. has_direction_param = false;
  218. }
  219. tokens.discard_whitespace();
  220. if (!tokens.has_next_token())
  221. return nullptr;
  222. if (has_direction_param && !tokens.consume_a_token().is(Token::Type::Comma))
  223. return nullptr;
  224. auto color_stops = parse_linear_color_stop_list(tokens);
  225. if (!color_stops.has_value())
  226. return nullptr;
  227. transaction.commit();
  228. return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient);
  229. }
  230. RefPtr<CSSStyleValue> Parser::parse_conic_gradient_function(TokenStream<ComponentValue>& outer_tokens)
  231. {
  232. auto transaction = outer_tokens.begin_transaction();
  233. auto& component_value = outer_tokens.consume_a_token();
  234. if (!component_value.is_function())
  235. return nullptr;
  236. GradientRepeating repeating_gradient = GradientRepeating::No;
  237. auto function_name = component_value.function().name().bytes_as_string_view();
  238. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  239. repeating_gradient = GradientRepeating::Yes;
  240. });
  241. if (!function_name.equals_ignoring_ascii_case("conic-gradient"sv))
  242. return nullptr;
  243. TokenStream tokens { component_value.function().values() };
  244. tokens.discard_whitespace();
  245. if (!tokens.has_next_token())
  246. return nullptr;
  247. Angle from_angle(0, Angle::Type::Deg);
  248. RefPtr<PositionStyleValue> at_position;
  249. // conic-gradient( [ [ from <angle> ]? [ at <position> ]? ] ||
  250. // <color-interpolation-method> , <angular-color-stop-list> )
  251. auto token = tokens.next_token();
  252. bool got_from_angle = false;
  253. bool got_color_interpolation_method = false;
  254. bool got_at_position = false;
  255. while (token.is(Token::Type::Ident)) {
  256. auto consume_identifier = [&](auto identifier) {
  257. auto token_string = token.token().ident();
  258. if (token_string.equals_ignoring_ascii_case(identifier)) {
  259. tokens.discard_a_token();
  260. tokens.discard_whitespace();
  261. return true;
  262. }
  263. return false;
  264. };
  265. if (consume_identifier("from"sv)) {
  266. // from <angle>
  267. if (got_from_angle || got_at_position)
  268. return nullptr;
  269. if (!tokens.has_next_token())
  270. return nullptr;
  271. auto angle_token = tokens.consume_a_token();
  272. if (!angle_token.is(Token::Type::Dimension))
  273. return nullptr;
  274. auto angle = angle_token.token().dimension_value();
  275. auto angle_unit = angle_token.token().dimension_unit();
  276. auto angle_type = Angle::unit_from_name(angle_unit);
  277. if (!angle_type.has_value())
  278. return nullptr;
  279. from_angle = Angle(angle, *angle_type);
  280. got_from_angle = true;
  281. } else if (consume_identifier("at"sv)) {
  282. // at <position>
  283. if (got_at_position)
  284. return nullptr;
  285. auto position = parse_position_value(tokens);
  286. if (!position)
  287. return nullptr;
  288. at_position = position;
  289. got_at_position = true;
  290. } else if (consume_identifier("in"sv)) {
  291. // <color-interpolation-method>
  292. if (got_color_interpolation_method)
  293. return nullptr;
  294. dbgln("FIXME: Parse color interpolation method for conic-gradient()");
  295. got_color_interpolation_method = true;
  296. } else {
  297. break;
  298. }
  299. tokens.discard_whitespace();
  300. if (!tokens.has_next_token())
  301. return nullptr;
  302. token = tokens.next_token();
  303. }
  304. tokens.discard_whitespace();
  305. if (!tokens.has_next_token())
  306. return nullptr;
  307. if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.consume_a_token().is(Token::Type::Comma))
  308. return nullptr;
  309. auto color_stops = parse_angular_color_stop_list(tokens);
  310. if (!color_stops.has_value())
  311. return nullptr;
  312. if (!at_position)
  313. at_position = PositionStyleValue::create_center();
  314. transaction.commit();
  315. return ConicGradientStyleValue::create(from_angle, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
  316. }
  317. RefPtr<CSSStyleValue> Parser::parse_radial_gradient_function(TokenStream<ComponentValue>& outer_tokens)
  318. {
  319. using EndingShape = RadialGradientStyleValue::EndingShape;
  320. using Extent = RadialGradientStyleValue::Extent;
  321. using CircleSize = RadialGradientStyleValue::CircleSize;
  322. using EllipseSize = RadialGradientStyleValue::EllipseSize;
  323. using Size = RadialGradientStyleValue::Size;
  324. auto transaction = outer_tokens.begin_transaction();
  325. auto& component_value = outer_tokens.consume_a_token();
  326. if (!component_value.is_function())
  327. return nullptr;
  328. auto repeating_gradient = GradientRepeating::No;
  329. auto function_name = component_value.function().name().bytes_as_string_view();
  330. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  331. repeating_gradient = GradientRepeating::Yes;
  332. });
  333. if (!function_name.equals_ignoring_ascii_case("radial-gradient"sv))
  334. return nullptr;
  335. TokenStream tokens { component_value.function().values() };
  336. tokens.discard_whitespace();
  337. if (!tokens.has_next_token())
  338. return nullptr;
  339. bool expect_comma = false;
  340. auto commit_value = [&]<typename... T>(auto value, T&... transactions) {
  341. (transactions.commit(), ...);
  342. return value;
  343. };
  344. // radial-gradient( [ <ending-shape> || <size> ]? [ at <position> ]? , <color-stop-list> )
  345. Size size = Extent::FarthestCorner;
  346. EndingShape ending_shape = EndingShape::Circle;
  347. RefPtr<PositionStyleValue> at_position;
  348. auto parse_ending_shape = [&]() -> Optional<EndingShape> {
  349. auto transaction = tokens.begin_transaction();
  350. tokens.discard_whitespace();
  351. auto& token = tokens.consume_a_token();
  352. if (!token.is(Token::Type::Ident))
  353. return {};
  354. auto ident = token.token().ident();
  355. if (ident.equals_ignoring_ascii_case("circle"sv))
  356. return commit_value(EndingShape::Circle, transaction);
  357. if (ident.equals_ignoring_ascii_case("ellipse"sv))
  358. return commit_value(EndingShape::Ellipse, transaction);
  359. return {};
  360. };
  361. auto parse_extent_keyword = [](StringView keyword) -> Optional<Extent> {
  362. if (keyword.equals_ignoring_ascii_case("closest-corner"sv))
  363. return Extent::ClosestCorner;
  364. if (keyword.equals_ignoring_ascii_case("closest-side"sv))
  365. return Extent::ClosestSide;
  366. if (keyword.equals_ignoring_ascii_case("farthest-corner"sv))
  367. return Extent::FarthestCorner;
  368. if (keyword.equals_ignoring_ascii_case("farthest-side"sv))
  369. return Extent::FarthestSide;
  370. return {};
  371. };
  372. auto parse_size = [&]() -> Optional<Size> {
  373. // <size> =
  374. // <extent-keyword> |
  375. // <length [0,∞]> |
  376. // <length-percentage [0,∞]>{2}
  377. auto transaction_size = tokens.begin_transaction();
  378. tokens.discard_whitespace();
  379. if (!tokens.has_next_token())
  380. return {};
  381. if (tokens.next_token().is(Token::Type::Ident)) {
  382. auto extent = parse_extent_keyword(tokens.consume_a_token().token().ident());
  383. if (!extent.has_value())
  384. return {};
  385. return commit_value(*extent, transaction_size);
  386. }
  387. auto first_radius = parse_length_percentage(tokens);
  388. if (!first_radius.has_value())
  389. return {};
  390. auto transaction_second_dimension = tokens.begin_transaction();
  391. tokens.discard_whitespace();
  392. if (tokens.has_next_token()) {
  393. auto second_radius = parse_length_percentage(tokens);
  394. if (second_radius.has_value())
  395. return commit_value(EllipseSize { first_radius.release_value(), second_radius.release_value() },
  396. transaction_size, transaction_second_dimension);
  397. }
  398. // FIXME: Support calculated lengths
  399. if (first_radius->is_length())
  400. return commit_value(CircleSize { first_radius->length() }, transaction_size);
  401. return {};
  402. };
  403. {
  404. // [ <ending-shape> || <size> ]?
  405. auto maybe_ending_shape = parse_ending_shape();
  406. auto maybe_size = parse_size();
  407. if (!maybe_ending_shape.has_value() && maybe_size.has_value())
  408. maybe_ending_shape = parse_ending_shape();
  409. if (maybe_size.has_value()) {
  410. size = *maybe_size;
  411. expect_comma = true;
  412. }
  413. if (maybe_ending_shape.has_value()) {
  414. expect_comma = true;
  415. ending_shape = *maybe_ending_shape;
  416. if (ending_shape == EndingShape::Circle && size.has<EllipseSize>())
  417. return nullptr;
  418. if (ending_shape == EndingShape::Ellipse && size.has<CircleSize>())
  419. return nullptr;
  420. } else {
  421. ending_shape = size.has<CircleSize>() ? EndingShape::Circle : EndingShape::Ellipse;
  422. }
  423. }
  424. tokens.discard_whitespace();
  425. if (!tokens.has_next_token())
  426. return nullptr;
  427. auto& token = tokens.next_token();
  428. if (token.is_ident("at"sv)) {
  429. tokens.discard_a_token();
  430. auto position = parse_position_value(tokens);
  431. if (!position)
  432. return nullptr;
  433. at_position = position;
  434. expect_comma = true;
  435. }
  436. tokens.discard_whitespace();
  437. if (!tokens.has_next_token())
  438. return nullptr;
  439. if (expect_comma && !tokens.consume_a_token().is(Token::Type::Comma))
  440. return nullptr;
  441. // <color-stop-list>
  442. auto color_stops = parse_linear_color_stop_list(tokens);
  443. if (!color_stops.has_value())
  444. return nullptr;
  445. if (!at_position)
  446. at_position = PositionStyleValue::create_center();
  447. transaction.commit();
  448. return RadialGradientStyleValue::create(ending_shape, size, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
  449. }
  450. }