GradientParsing.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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. auto const& token = tokens.next_token();
  30. RefPtr<StyleValue> color;
  31. Optional<typename TElement::PositionType> position;
  32. Optional<typename TElement::PositionType> second_position;
  33. auto dimension = parse_dimension(token);
  34. if (dimension.has_value() && is_position(*dimension)) {
  35. // [<T-percentage> <color>] or [<T-percentage>]
  36. position = get_position(*dimension);
  37. tokens.skip_whitespace();
  38. // <T-percentage>
  39. if (!tokens.has_next_token() || tokens.peek_token().is(Token::Type::Comma)) {
  40. element.transition_hint = typename TElement::ColorHint { *position };
  41. return ElementType::ColorHint;
  42. }
  43. // <T-percentage> <color>
  44. auto maybe_color = parse_color_value(tokens.next_token());
  45. if (!maybe_color)
  46. return ElementType::Garbage;
  47. color = maybe_color.release_nonnull();
  48. } else {
  49. // [<color> <T-percentage>?]
  50. auto maybe_color = parse_color_value(token);
  51. if (!maybe_color)
  52. return ElementType::Garbage;
  53. color = maybe_color.release_nonnull();
  54. tokens.skip_whitespace();
  55. // Allow up to [<color> <T-percentage> <T-percentage>] (double-position color stops)
  56. // Note: Double-position color stops only appear to be valid in this order.
  57. for (auto stop_position : Array { &position, &second_position }) {
  58. if (tokens.has_next_token() && !tokens.peek_token().is(Token::Type::Comma)) {
  59. auto token = tokens.next_token();
  60. auto dimension = parse_dimension(token);
  61. if (!dimension.has_value() || !is_position(*dimension))
  62. return ElementType::Garbage;
  63. *stop_position = get_position(*dimension);
  64. tokens.skip_whitespace();
  65. }
  66. }
  67. }
  68. element.color_stop = typename TElement::ColorStop { color, position, second_position };
  69. return ElementType::ColorStop;
  70. };
  71. TElement first_element {};
  72. if (parse_color_stop_list_element(first_element) != ElementType::ColorStop)
  73. return {};
  74. if (!tokens.has_next_token())
  75. return {};
  76. Vector<TElement> color_stops { first_element };
  77. while (tokens.has_next_token()) {
  78. TElement list_element {};
  79. tokens.skip_whitespace();
  80. if (!tokens.next_token().is(Token::Type::Comma))
  81. return {};
  82. auto element_type = parse_color_stop_list_element(list_element);
  83. if (element_type == ElementType::ColorHint) {
  84. // <color-hint>, <color-stop>
  85. tokens.skip_whitespace();
  86. if (!tokens.next_token().is(Token::Type::Comma))
  87. return {};
  88. // Note: This fills in the color stop on the same list_element as the color hint (it does not overwrite it).
  89. if (parse_color_stop_list_element(list_element) != ElementType::ColorStop)
  90. return {};
  91. } else if (element_type == ElementType::ColorStop) {
  92. // <color-stop>
  93. } else {
  94. return {};
  95. }
  96. color_stops.append(list_element);
  97. }
  98. return color_stops;
  99. }
  100. static StringView consume_if_starts_with(StringView str, StringView start, auto found_callback)
  101. {
  102. if (str.starts_with(start, CaseSensitivity::CaseInsensitive)) {
  103. found_callback();
  104. return str.substring_view(start.length());
  105. }
  106. return str;
  107. }
  108. Optional<Vector<LinearColorStopListElement>> Parser::parse_linear_color_stop_list(TokenStream<ComponentValue>& tokens)
  109. {
  110. // <color-stop-list> =
  111. // <linear-color-stop> , [ <linear-color-hint>? , <linear-color-stop> ]#
  112. return parse_color_stop_list<LinearColorStopListElement>(
  113. tokens,
  114. [](Dimension& dimension) { return dimension.is_length_percentage(); },
  115. [](Dimension& dimension) { return dimension.length_percentage(); });
  116. }
  117. Optional<Vector<AngularColorStopListElement>> Parser::parse_angular_color_stop_list(TokenStream<ComponentValue>& tokens)
  118. {
  119. // <angular-color-stop-list> =
  120. // <angular-color-stop> , [ <angular-color-hint>? , <angular-color-stop> ]#
  121. return parse_color_stop_list<AngularColorStopListElement>(
  122. tokens,
  123. [](Dimension& dimension) { return dimension.is_angle_percentage(); },
  124. [](Dimension& dimension) { return dimension.angle_percentage(); });
  125. }
  126. RefPtr<StyleValue> Parser::parse_linear_gradient_function(ComponentValue const& component_value)
  127. {
  128. using GradientType = LinearGradientStyleValue::GradientType;
  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.skip_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.peek_token();
  170. if (first_param.is(Token::Type::Dimension)) {
  171. // <angle>
  172. tokens.next_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.next_token();
  184. tokens.skip_whitespace();
  185. if (!tokens.has_next_token())
  186. return nullptr;
  187. }
  188. // [left | right] || [top | bottom]
  189. auto const& first_side = tokens.next_token();
  190. if (!first_side.is(Token::Type::Ident))
  191. return nullptr;
  192. auto side_a = to_side(first_side.token().ident());
  193. tokens.skip_whitespace();
  194. Optional<SideOrCorner> side_b;
  195. if (tokens.has_next_token() && tokens.peek_token().is(Token::Type::Ident))
  196. side_b = to_side(tokens.next_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.skip_whitespace();
  220. if (!tokens.has_next_token())
  221. return nullptr;
  222. if (has_direction_param && !tokens.next_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. return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient);
  228. }
  229. RefPtr<StyleValue> Parser::parse_conic_gradient_function(ComponentValue const& component_value)
  230. {
  231. if (!component_value.is_function())
  232. return nullptr;
  233. GradientRepeating repeating_gradient = GradientRepeating::No;
  234. auto function_name = component_value.function().name().bytes_as_string_view();
  235. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  236. repeating_gradient = GradientRepeating::Yes;
  237. });
  238. if (!function_name.equals_ignoring_ascii_case("conic-gradient"sv))
  239. return nullptr;
  240. TokenStream tokens { component_value.function().values() };
  241. tokens.skip_whitespace();
  242. if (!tokens.has_next_token())
  243. return nullptr;
  244. Angle from_angle(0, Angle::Type::Deg);
  245. RefPtr<PositionStyleValue> at_position;
  246. // conic-gradient( [ [ from <angle> ]? [ at <position> ]? ] ||
  247. // <color-interpolation-method> , <angular-color-stop-list> )
  248. auto token = tokens.peek_token();
  249. bool got_from_angle = false;
  250. bool got_color_interpolation_method = false;
  251. bool got_at_position = false;
  252. while (token.is(Token::Type::Ident)) {
  253. auto consume_identifier = [&](auto identifier) {
  254. auto token_string = token.token().ident();
  255. if (token_string.equals_ignoring_ascii_case(identifier)) {
  256. (void)tokens.next_token();
  257. tokens.skip_whitespace();
  258. return true;
  259. }
  260. return false;
  261. };
  262. if (consume_identifier("from"sv)) {
  263. // from <angle>
  264. if (got_from_angle || got_at_position)
  265. return nullptr;
  266. if (!tokens.has_next_token())
  267. return nullptr;
  268. auto angle_token = tokens.next_token();
  269. if (!angle_token.is(Token::Type::Dimension))
  270. return nullptr;
  271. auto angle = angle_token.token().dimension_value();
  272. auto angle_unit = angle_token.token().dimension_unit();
  273. auto angle_type = Angle::unit_from_name(angle_unit);
  274. if (!angle_type.has_value())
  275. return nullptr;
  276. from_angle = Angle(angle, *angle_type);
  277. got_from_angle = true;
  278. } else if (consume_identifier("at"sv)) {
  279. // at <position>
  280. if (got_at_position)
  281. return nullptr;
  282. auto position = parse_position_value(tokens);
  283. if (!position)
  284. return nullptr;
  285. at_position = position;
  286. got_at_position = true;
  287. } else if (consume_identifier("in"sv)) {
  288. // <color-interpolation-method>
  289. if (got_color_interpolation_method)
  290. return nullptr;
  291. dbgln("FIXME: Parse color interpolation method for conic-gradient()");
  292. got_color_interpolation_method = true;
  293. } else {
  294. break;
  295. }
  296. tokens.skip_whitespace();
  297. if (!tokens.has_next_token())
  298. return nullptr;
  299. token = tokens.peek_token();
  300. }
  301. tokens.skip_whitespace();
  302. if (!tokens.has_next_token())
  303. return nullptr;
  304. if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.next_token().is(Token::Type::Comma))
  305. return nullptr;
  306. auto color_stops = parse_angular_color_stop_list(tokens);
  307. if (!color_stops.has_value())
  308. return nullptr;
  309. if (!at_position)
  310. at_position = PositionStyleValue::create_center();
  311. return ConicGradientStyleValue::create(from_angle, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
  312. }
  313. RefPtr<StyleValue> Parser::parse_radial_gradient_function(ComponentValue const& component_value)
  314. {
  315. using EndingShape = RadialGradientStyleValue::EndingShape;
  316. using Extent = RadialGradientStyleValue::Extent;
  317. using CircleSize = RadialGradientStyleValue::CircleSize;
  318. using EllipseSize = RadialGradientStyleValue::EllipseSize;
  319. using Size = RadialGradientStyleValue::Size;
  320. if (!component_value.is_function())
  321. return nullptr;
  322. auto repeating_gradient = GradientRepeating::No;
  323. auto function_name = component_value.function().name().bytes_as_string_view();
  324. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  325. repeating_gradient = GradientRepeating::Yes;
  326. });
  327. if (!function_name.equals_ignoring_ascii_case("radial-gradient"sv))
  328. return nullptr;
  329. TokenStream tokens { component_value.function().values() };
  330. tokens.skip_whitespace();
  331. if (!tokens.has_next_token())
  332. return nullptr;
  333. bool expect_comma = false;
  334. auto commit_value = [&]<typename... T>(auto value, T&... transactions) {
  335. (transactions.commit(), ...);
  336. return value;
  337. };
  338. // radial-gradient( [ <ending-shape> || <size> ]? [ at <position> ]? , <color-stop-list> )
  339. Size size = Extent::FarthestCorner;
  340. EndingShape ending_shape = EndingShape::Circle;
  341. RefPtr<PositionStyleValue> at_position;
  342. auto parse_ending_shape = [&]() -> Optional<EndingShape> {
  343. auto transaction = tokens.begin_transaction();
  344. tokens.skip_whitespace();
  345. auto& token = tokens.next_token();
  346. if (!token.is(Token::Type::Ident))
  347. return {};
  348. auto ident = token.token().ident();
  349. if (ident.equals_ignoring_ascii_case("circle"sv))
  350. return commit_value(EndingShape::Circle, transaction);
  351. if (ident.equals_ignoring_ascii_case("ellipse"sv))
  352. return commit_value(EndingShape::Ellipse, transaction);
  353. return {};
  354. };
  355. auto parse_extent_keyword = [](StringView keyword) -> Optional<Extent> {
  356. if (keyword.equals_ignoring_ascii_case("closest-corner"sv))
  357. return Extent::ClosestCorner;
  358. if (keyword.equals_ignoring_ascii_case("closest-side"sv))
  359. return Extent::ClosestSide;
  360. if (keyword.equals_ignoring_ascii_case("farthest-corner"sv))
  361. return Extent::FarthestCorner;
  362. if (keyword.equals_ignoring_ascii_case("farthest-side"sv))
  363. return Extent::FarthestSide;
  364. return {};
  365. };
  366. auto parse_size = [&]() -> Optional<Size> {
  367. // <size> =
  368. // <extent-keyword> |
  369. // <length [0,∞]> |
  370. // <length-percentage [0,∞]>{2}
  371. auto transaction_size = tokens.begin_transaction();
  372. tokens.skip_whitespace();
  373. if (!tokens.has_next_token())
  374. return {};
  375. if (tokens.peek_token().is(Token::Type::Ident)) {
  376. auto extent = parse_extent_keyword(tokens.next_token().token().ident());
  377. if (!extent.has_value())
  378. return {};
  379. return commit_value(*extent, transaction_size);
  380. }
  381. auto first_radius = parse_length_percentage(tokens);
  382. if (!first_radius.has_value())
  383. return {};
  384. auto transaction_second_dimension = tokens.begin_transaction();
  385. tokens.skip_whitespace();
  386. if (tokens.has_next_token()) {
  387. auto second_radius = parse_length_percentage(tokens);
  388. if (second_radius.has_value())
  389. return commit_value(EllipseSize { first_radius.release_value(), second_radius.release_value() },
  390. transaction_size, transaction_second_dimension);
  391. }
  392. // FIXME: Support calculated lengths
  393. if (first_radius->is_length())
  394. return commit_value(CircleSize { first_radius->length() }, transaction_size);
  395. return {};
  396. };
  397. {
  398. // [ <ending-shape> || <size> ]?
  399. auto maybe_ending_shape = parse_ending_shape();
  400. auto maybe_size = parse_size();
  401. if (!maybe_ending_shape.has_value() && maybe_size.has_value())
  402. maybe_ending_shape = parse_ending_shape();
  403. if (maybe_size.has_value()) {
  404. size = *maybe_size;
  405. expect_comma = true;
  406. }
  407. if (maybe_ending_shape.has_value()) {
  408. expect_comma = true;
  409. ending_shape = *maybe_ending_shape;
  410. if (ending_shape == EndingShape::Circle && size.has<EllipseSize>())
  411. return nullptr;
  412. if (ending_shape == EndingShape::Ellipse && size.has<CircleSize>())
  413. return nullptr;
  414. } else {
  415. ending_shape = size.has<CircleSize>() ? EndingShape::Circle : EndingShape::Ellipse;
  416. }
  417. }
  418. tokens.skip_whitespace();
  419. if (!tokens.has_next_token())
  420. return nullptr;
  421. auto& token = tokens.peek_token();
  422. if (token.is_ident("at"sv)) {
  423. (void)tokens.next_token();
  424. auto position = parse_position_value(tokens);
  425. if (!position)
  426. return nullptr;
  427. at_position = position;
  428. expect_comma = true;
  429. }
  430. tokens.skip_whitespace();
  431. if (!tokens.has_next_token())
  432. return nullptr;
  433. if (expect_comma && !tokens.next_token().is(Token::Type::Comma))
  434. return nullptr;
  435. // <color-stop-list>
  436. auto color_stops = parse_linear_color_stop_list(tokens);
  437. if (!color_stops.has_value())
  438. return nullptr;
  439. if (!at_position)
  440. at_position = PositionStyleValue::create_center();
  441. return RadialGradientStyleValue::create(ending_shape, size, at_position.release_nonnull(), move(*color_stops), repeating_gradient);
  442. }
  443. }