GradientParsing.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. /*
  2. * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.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.skip_whitespace();
  27. if (!tokens.has_next_token())
  28. return ElementType::Garbage;
  29. RefPtr<StyleValue> color;
  30. Optional<typename TElement::PositionType> position;
  31. Optional<typename TElement::PositionType> second_position;
  32. if (auto dimension = parse_dimension(tokens.peek_token()); dimension.has_value() && is_position(*dimension)) {
  33. // [<T-percentage> <color>] or [<T-percentage>]
  34. position = get_position(*dimension);
  35. (void)tokens.next_token(); // dimension
  36. tokens.skip_whitespace();
  37. // <T-percentage>
  38. if (!tokens.has_next_token() || tokens.peek_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.skip_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.peek_token().is(Token::Type::Comma)) {
  58. auto dimension = parse_dimension(tokens.next_token());
  59. if (!dimension.has_value() || !is_position(*dimension))
  60. return ElementType::Garbage;
  61. *stop_position = get_position(*dimension);
  62. tokens.skip_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.skip_whitespace();
  78. if (!tokens.next_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.skip_whitespace();
  84. if (!tokens.next_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<StyleValue> Parser::parse_linear_gradient_function(ComponentValue const& component_value)
  125. {
  126. using GradientType = LinearGradientStyleValue::GradientType;
  127. if (!component_value.is_function())
  128. return nullptr;
  129. GradientRepeating repeating_gradient = GradientRepeating::No;
  130. GradientType gradient_type { GradientType::Standard };
  131. auto function_name = component_value.function().name().bytes_as_string_view();
  132. function_name = consume_if_starts_with(function_name, "-webkit-"sv, [&] {
  133. gradient_type = GradientType::WebKit;
  134. });
  135. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  136. repeating_gradient = GradientRepeating::Yes;
  137. });
  138. if (!function_name.equals_ignoring_ascii_case("linear-gradient"sv))
  139. return nullptr;
  140. // linear-gradient() = linear-gradient([ <angle> | to <side-or-corner> ]?, <color-stop-list>)
  141. TokenStream tokens { component_value.function().values() };
  142. tokens.skip_whitespace();
  143. if (!tokens.has_next_token())
  144. return nullptr;
  145. bool has_direction_param = true;
  146. LinearGradientStyleValue::GradientDirection gradient_direction = gradient_type == GradientType::Standard
  147. ? SideOrCorner::Bottom
  148. : SideOrCorner::Top;
  149. auto to_side = [](StringView value) -> Optional<SideOrCorner> {
  150. if (value.equals_ignoring_ascii_case("top"sv))
  151. return SideOrCorner::Top;
  152. if (value.equals_ignoring_ascii_case("bottom"sv))
  153. return SideOrCorner::Bottom;
  154. if (value.equals_ignoring_ascii_case("left"sv))
  155. return SideOrCorner::Left;
  156. if (value.equals_ignoring_ascii_case("right"sv))
  157. return SideOrCorner::Right;
  158. return {};
  159. };
  160. auto is_to_side_or_corner = [&](auto const& token) {
  161. if (!token.is(Token::Type::Ident))
  162. return false;
  163. if (gradient_type == GradientType::WebKit)
  164. return to_side(token.token().ident()).has_value();
  165. return token.token().ident().equals_ignoring_ascii_case("to"sv);
  166. };
  167. auto const& first_param = tokens.peek_token();
  168. if (first_param.is(Token::Type::Dimension)) {
  169. // <angle>
  170. tokens.next_token();
  171. auto angle_value = first_param.token().dimension_value();
  172. auto unit_string = first_param.token().dimension_unit();
  173. auto angle_type = Angle::unit_from_name(unit_string);
  174. if (!angle_type.has_value())
  175. return nullptr;
  176. gradient_direction = Angle { angle_value, angle_type.release_value() };
  177. } else if (is_to_side_or_corner(first_param)) {
  178. // <side-or-corner> = [left | right] || [top | bottom]
  179. // Note: -webkit-linear-gradient does not include to the "to" prefix on the side or corner
  180. if (gradient_type == GradientType::Standard) {
  181. tokens.next_token();
  182. tokens.skip_whitespace();
  183. if (!tokens.has_next_token())
  184. return nullptr;
  185. }
  186. // [left | right] || [top | bottom]
  187. auto const& first_side = tokens.next_token();
  188. if (!first_side.is(Token::Type::Ident))
  189. return nullptr;
  190. auto side_a = to_side(first_side.token().ident());
  191. tokens.skip_whitespace();
  192. Optional<SideOrCorner> side_b;
  193. if (tokens.has_next_token() && tokens.peek_token().is(Token::Type::Ident))
  194. side_b = to_side(tokens.next_token().token().ident());
  195. if (side_a.has_value() && !side_b.has_value()) {
  196. gradient_direction = *side_a;
  197. } else if (side_a.has_value() && side_b.has_value()) {
  198. // Convert two sides to a corner
  199. if (to_underlying(*side_b) < to_underlying(*side_a))
  200. swap(side_a, side_b);
  201. if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Left)
  202. gradient_direction = SideOrCorner::TopLeft;
  203. else if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Right)
  204. gradient_direction = SideOrCorner::TopRight;
  205. else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Left)
  206. gradient_direction = SideOrCorner::BottomLeft;
  207. else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Right)
  208. gradient_direction = SideOrCorner::BottomRight;
  209. else
  210. return nullptr;
  211. } else {
  212. return nullptr;
  213. }
  214. } else {
  215. has_direction_param = false;
  216. }
  217. tokens.skip_whitespace();
  218. if (!tokens.has_next_token())
  219. return nullptr;
  220. if (has_direction_param && !tokens.next_token().is(Token::Type::Comma))
  221. return nullptr;
  222. auto color_stops = parse_linear_color_stop_list(tokens);
  223. if (!color_stops.has_value())
  224. return nullptr;
  225. return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient);
  226. }
  227. RefPtr<StyleValue> Parser::parse_conic_gradient_function(ComponentValue const& component_value)
  228. {
  229. if (!component_value.is_function())
  230. return nullptr;
  231. GradientRepeating repeating_gradient = GradientRepeating::No;
  232. auto function_name = component_value.function().name().bytes_as_string_view();
  233. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  234. repeating_gradient = GradientRepeating::Yes;
  235. });
  236. if (!function_name.equals_ignoring_ascii_case("conic-gradient"sv))
  237. return nullptr;
  238. TokenStream tokens { component_value.function().values() };
  239. tokens.skip_whitespace();
  240. if (!tokens.has_next_token())
  241. return nullptr;
  242. Angle from_angle(0, Angle::Type::Deg);
  243. RefPtr<PositionStyleValue> at_position;
  244. // conic-gradient( [ [ from <angle> ]? [ at <position> ]? ] ||
  245. // <color-interpolation-method> , <angular-color-stop-list> )
  246. auto token = tokens.peek_token();
  247. bool got_from_angle = false;
  248. bool got_color_interpolation_method = false;
  249. bool got_at_position = false;
  250. while (token.is(Token::Type::Ident)) {
  251. auto consume_identifier = [&](auto identifier) {
  252. auto token_string = token.token().ident();
  253. if (token_string.equals_ignoring_ascii_case(identifier)) {
  254. (void)tokens.next_token();
  255. tokens.skip_whitespace();
  256. return true;
  257. }
  258. return false;
  259. };
  260. if (consume_identifier("from"sv)) {
  261. // from <angle>
  262. if (got_from_angle || got_at_position)
  263. return nullptr;
  264. if (!tokens.has_next_token())
  265. return nullptr;
  266. auto angle_token = tokens.next_token();
  267. if (!angle_token.is(Token::Type::Dimension))
  268. return nullptr;
  269. auto angle = angle_token.token().dimension_value();
  270. auto angle_unit = angle_token.token().dimension_unit();
  271. auto angle_type = Angle::unit_from_name(angle_unit);
  272. if (!angle_type.has_value())
  273. return nullptr;
  274. from_angle = Angle(angle, *angle_type);
  275. got_from_angle = true;
  276. } else if (consume_identifier("at"sv)) {
  277. // at <position>
  278. if (got_at_position)
  279. return nullptr;
  280. auto position = parse_position_value(tokens);
  281. if (!position)
  282. return nullptr;
  283. at_position = position;
  284. got_at_position = true;
  285. } else if (consume_identifier("in"sv)) {
  286. // <color-interpolation-method>
  287. if (got_color_interpolation_method)
  288. return nullptr;
  289. dbgln("FIXME: Parse color interpolation method for conic-gradient()");
  290. got_color_interpolation_method = true;
  291. } else {
  292. break;
  293. }
  294. tokens.skip_whitespace();
  295. if (!tokens.has_next_token())
  296. return nullptr;
  297. token = tokens.peek_token();
  298. }
  299. tokens.skip_whitespace();
  300. if (!tokens.has_next_token())
  301. return nullptr;
  302. if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.next_token().is(Token::Type::Comma))
  303. return nullptr;
  304. auto color_stops = parse_angular_color_stop_list(tokens);
  305. if (!color_stops.has_value())
  306. return nullptr;
  307. if (!at_position)
  308. at_position = PositionStyleValue::create_center();
  309. return ConicGradientStyleValue::create(from_angle, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
  310. }
  311. RefPtr<StyleValue> Parser::parse_radial_gradient_function(ComponentValue const& component_value)
  312. {
  313. using EndingShape = RadialGradientStyleValue::EndingShape;
  314. using Extent = RadialGradientStyleValue::Extent;
  315. using CircleSize = RadialGradientStyleValue::CircleSize;
  316. using EllipseSize = RadialGradientStyleValue::EllipseSize;
  317. using Size = RadialGradientStyleValue::Size;
  318. if (!component_value.is_function())
  319. return nullptr;
  320. auto repeating_gradient = GradientRepeating::No;
  321. auto function_name = component_value.function().name().bytes_as_string_view();
  322. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  323. repeating_gradient = GradientRepeating::Yes;
  324. });
  325. if (!function_name.equals_ignoring_ascii_case("radial-gradient"sv))
  326. return nullptr;
  327. TokenStream tokens { component_value.function().values() };
  328. tokens.skip_whitespace();
  329. if (!tokens.has_next_token())
  330. return nullptr;
  331. bool expect_comma = false;
  332. auto commit_value = [&]<typename... T>(auto value, T&... transactions) {
  333. (transactions.commit(), ...);
  334. return value;
  335. };
  336. // radial-gradient( [ <ending-shape> || <size> ]? [ at <position> ]? , <color-stop-list> )
  337. Size size = Extent::FarthestCorner;
  338. EndingShape ending_shape = EndingShape::Circle;
  339. RefPtr<PositionStyleValue> at_position;
  340. auto parse_ending_shape = [&]() -> Optional<EndingShape> {
  341. auto transaction = tokens.begin_transaction();
  342. tokens.skip_whitespace();
  343. auto& token = tokens.next_token();
  344. if (!token.is(Token::Type::Ident))
  345. return {};
  346. auto ident = token.token().ident();
  347. if (ident.equals_ignoring_ascii_case("circle"sv))
  348. return commit_value(EndingShape::Circle, transaction);
  349. if (ident.equals_ignoring_ascii_case("ellipse"sv))
  350. return commit_value(EndingShape::Ellipse, transaction);
  351. return {};
  352. };
  353. auto parse_extent_keyword = [](StringView keyword) -> Optional<Extent> {
  354. if (keyword.equals_ignoring_ascii_case("closest-corner"sv))
  355. return Extent::ClosestCorner;
  356. if (keyword.equals_ignoring_ascii_case("closest-side"sv))
  357. return Extent::ClosestSide;
  358. if (keyword.equals_ignoring_ascii_case("farthest-corner"sv))
  359. return Extent::FarthestCorner;
  360. if (keyword.equals_ignoring_ascii_case("farthest-side"sv))
  361. return Extent::FarthestSide;
  362. return {};
  363. };
  364. auto parse_size = [&]() -> Optional<Size> {
  365. // <size> =
  366. // <extent-keyword> |
  367. // <length [0,∞]> |
  368. // <length-percentage [0,∞]>{2}
  369. auto transaction_size = tokens.begin_transaction();
  370. tokens.skip_whitespace();
  371. if (!tokens.has_next_token())
  372. return {};
  373. if (tokens.peek_token().is(Token::Type::Ident)) {
  374. auto extent = parse_extent_keyword(tokens.next_token().token().ident());
  375. if (!extent.has_value())
  376. return {};
  377. return commit_value(*extent, transaction_size);
  378. }
  379. auto first_radius = parse_length_percentage(tokens);
  380. if (!first_radius.has_value())
  381. return {};
  382. auto transaction_second_dimension = tokens.begin_transaction();
  383. tokens.skip_whitespace();
  384. if (tokens.has_next_token()) {
  385. auto second_radius = parse_length_percentage(tokens);
  386. if (second_radius.has_value())
  387. return commit_value(EllipseSize { first_radius.release_value(), second_radius.release_value() },
  388. transaction_size, transaction_second_dimension);
  389. }
  390. // FIXME: Support calculated lengths
  391. if (first_radius->is_length())
  392. return commit_value(CircleSize { first_radius->length() }, transaction_size);
  393. return {};
  394. };
  395. {
  396. // [ <ending-shape> || <size> ]?
  397. auto maybe_ending_shape = parse_ending_shape();
  398. auto maybe_size = parse_size();
  399. if (!maybe_ending_shape.has_value() && maybe_size.has_value())
  400. maybe_ending_shape = parse_ending_shape();
  401. if (maybe_size.has_value()) {
  402. size = *maybe_size;
  403. expect_comma = true;
  404. }
  405. if (maybe_ending_shape.has_value()) {
  406. expect_comma = true;
  407. ending_shape = *maybe_ending_shape;
  408. if (ending_shape == EndingShape::Circle && size.has<EllipseSize>())
  409. return nullptr;
  410. if (ending_shape == EndingShape::Ellipse && size.has<CircleSize>())
  411. return nullptr;
  412. } else {
  413. ending_shape = size.has<CircleSize>() ? EndingShape::Circle : EndingShape::Ellipse;
  414. }
  415. }
  416. tokens.skip_whitespace();
  417. if (!tokens.has_next_token())
  418. return nullptr;
  419. auto& token = tokens.peek_token();
  420. if (token.is_ident("at"sv)) {
  421. (void)tokens.next_token();
  422. auto position = parse_position_value(tokens);
  423. if (!position)
  424. return nullptr;
  425. at_position = position;
  426. expect_comma = true;
  427. }
  428. tokens.skip_whitespace();
  429. if (!tokens.has_next_token())
  430. return nullptr;
  431. if (expect_comma && !tokens.next_token().is(Token::Type::Comma))
  432. return nullptr;
  433. // <color-stop-list>
  434. auto color_stops = parse_linear_color_stop_list(tokens);
  435. if (!color_stops.has_value())
  436. return nullptr;
  437. if (!at_position)
  438. at_position = PositionStyleValue::create_center();
  439. return RadialGradientStyleValue::create(ending_shape, size, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
  440. }
  441. }