GradientParsing.cpp 20 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/RadialGradientStyleValue.h>
  15. namespace Web::CSS::Parser {
  16. template<typename TElement>
  17. static Optional<Vector<TElement>> parse_color_stop_list(auto& tokens, auto is_position, auto get_position, auto parse_color, auto parse_dimension)
  18. {
  19. enum class ElementType {
  20. Garbage,
  21. ColorStop,
  22. ColorHint
  23. };
  24. auto parse_color_stop_list_element = [&](TElement& element) -> ElementType {
  25. tokens.skip_whitespace();
  26. if (!tokens.has_next_token())
  27. return ElementType::Garbage;
  28. auto const& token = tokens.next_token();
  29. RefPtr<StyleValue> color;
  30. Optional<typename TElement::PositionType> position;
  31. Optional<typename TElement::PositionType> second_position;
  32. auto dimension = parse_dimension(token);
  33. if (dimension.has_value() && is_position(*dimension)) {
  34. // [<T-percentage> <color>] or [<T-percentage>]
  35. position = get_position(*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(tokens.next_token());
  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(token);
  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 token = tokens.next_token();
  59. auto dimension = parse_dimension(token);
  60. if (!dimension.has_value() || !is_position(*dimension))
  61. return ElementType::Garbage;
  62. *stop_position = get_position(*dimension);
  63. tokens.skip_whitespace();
  64. }
  65. }
  66. }
  67. element.color_stop = typename TElement::ColorStop { color, position, second_position };
  68. return ElementType::ColorStop;
  69. };
  70. TElement first_element {};
  71. if (parse_color_stop_list_element(first_element) != ElementType::ColorStop)
  72. return {};
  73. if (!tokens.has_next_token())
  74. return {};
  75. Vector<TElement> color_stops { first_element };
  76. while (tokens.has_next_token()) {
  77. TElement list_element {};
  78. tokens.skip_whitespace();
  79. if (!tokens.next_token().is(Token::Type::Comma))
  80. return {};
  81. auto element_type = parse_color_stop_list_element(list_element);
  82. if (element_type == ElementType::ColorHint) {
  83. // <color-hint>, <color-stop>
  84. tokens.skip_whitespace();
  85. if (!tokens.next_token().is(Token::Type::Comma))
  86. return {};
  87. // Note: This fills in the color stop on the same list_element as the color hint (it does not overwrite it).
  88. if (parse_color_stop_list_element(list_element) != ElementType::ColorStop)
  89. return {};
  90. } else if (element_type == ElementType::ColorStop) {
  91. // <color-stop>
  92. } else {
  93. return {};
  94. }
  95. color_stops.append(list_element);
  96. }
  97. return color_stops;
  98. }
  99. static StringView consume_if_starts_with(StringView str, StringView start, auto found_callback)
  100. {
  101. if (str.starts_with(start, CaseSensitivity::CaseInsensitive)) {
  102. found_callback();
  103. return str.substring_view(start.length());
  104. }
  105. return str;
  106. }
  107. Optional<Vector<LinearColorStopListElement>> Parser::parse_linear_color_stop_list(TokenStream<ComponentValue>& tokens)
  108. {
  109. // <color-stop-list> =
  110. // <linear-color-stop> , [ <linear-color-hint>? , <linear-color-stop> ]#
  111. return parse_color_stop_list<LinearColorStopListElement>(
  112. tokens,
  113. [](Dimension& dimension) { return dimension.is_length_percentage(); },
  114. [](Dimension& dimension) { return dimension.length_percentage(); },
  115. [&](auto& token) { return parse_color_value(token); },
  116. [&](auto& token) { return parse_dimension(token); });
  117. }
  118. Optional<Vector<AngularColorStopListElement>> Parser::parse_angular_color_stop_list(TokenStream<ComponentValue>& tokens)
  119. {
  120. // <angular-color-stop-list> =
  121. // <angular-color-stop> , [ <angular-color-hint>? , <angular-color-stop> ]#
  122. return parse_color_stop_list<AngularColorStopListElement>(
  123. tokens,
  124. [](Dimension& dimension) { return dimension.is_angle_percentage(); },
  125. [](Dimension& dimension) { return dimension.angle_percentage(); },
  126. [&](auto& token) { return parse_color_value(token); },
  127. [&](auto& token) { return parse_dimension(token); });
  128. }
  129. RefPtr<StyleValue> Parser::parse_linear_gradient_function(ComponentValue const& component_value)
  130. {
  131. using GradientType = LinearGradientStyleValue::GradientType;
  132. if (!component_value.is_function())
  133. return nullptr;
  134. GradientRepeating repeating_gradient = GradientRepeating::No;
  135. GradientType gradient_type { GradientType::Standard };
  136. auto function_name = component_value.function().name().bytes_as_string_view();
  137. function_name = consume_if_starts_with(function_name, "-webkit-"sv, [&] {
  138. gradient_type = GradientType::WebKit;
  139. });
  140. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  141. repeating_gradient = GradientRepeating::Yes;
  142. });
  143. if (!function_name.equals_ignoring_ascii_case("linear-gradient"sv))
  144. return nullptr;
  145. // linear-gradient() = linear-gradient([ <angle> | to <side-or-corner> ]?, <color-stop-list>)
  146. TokenStream tokens { component_value.function().values() };
  147. tokens.skip_whitespace();
  148. if (!tokens.has_next_token())
  149. return nullptr;
  150. bool has_direction_param = true;
  151. LinearGradientStyleValue::GradientDirection gradient_direction = gradient_type == GradientType::Standard
  152. ? SideOrCorner::Bottom
  153. : SideOrCorner::Top;
  154. auto to_side = [](StringView value) -> Optional<SideOrCorner> {
  155. if (value.equals_ignoring_ascii_case("top"sv))
  156. return SideOrCorner::Top;
  157. if (value.equals_ignoring_ascii_case("bottom"sv))
  158. return SideOrCorner::Bottom;
  159. if (value.equals_ignoring_ascii_case("left"sv))
  160. return SideOrCorner::Left;
  161. if (value.equals_ignoring_ascii_case("right"sv))
  162. return SideOrCorner::Right;
  163. return {};
  164. };
  165. auto is_to_side_or_corner = [&](auto const& token) {
  166. if (!token.is(Token::Type::Ident))
  167. return false;
  168. if (gradient_type == GradientType::WebKit)
  169. return to_side(token.token().ident()).has_value();
  170. return token.token().ident().equals_ignoring_ascii_case("to"sv);
  171. };
  172. auto const& first_param = tokens.peek_token();
  173. if (first_param.is(Token::Type::Dimension)) {
  174. // <angle>
  175. tokens.next_token();
  176. auto angle_value = first_param.token().dimension_value();
  177. auto unit_string = first_param.token().dimension_unit();
  178. auto angle_type = Angle::unit_from_name(unit_string);
  179. if (!angle_type.has_value())
  180. return nullptr;
  181. gradient_direction = Angle { angle_value, angle_type.release_value() };
  182. } else if (is_to_side_or_corner(first_param)) {
  183. // <side-or-corner> = [left | right] || [top | bottom]
  184. // Note: -webkit-linear-gradient does not include to the "to" prefix on the side or corner
  185. if (gradient_type == GradientType::Standard) {
  186. tokens.next_token();
  187. tokens.skip_whitespace();
  188. if (!tokens.has_next_token())
  189. return nullptr;
  190. }
  191. // [left | right] || [top | bottom]
  192. auto const& first_side = tokens.next_token();
  193. if (!first_side.is(Token::Type::Ident))
  194. return nullptr;
  195. auto side_a = to_side(first_side.token().ident());
  196. tokens.skip_whitespace();
  197. Optional<SideOrCorner> side_b;
  198. if (tokens.has_next_token() && tokens.peek_token().is(Token::Type::Ident))
  199. side_b = to_side(tokens.next_token().token().ident());
  200. if (side_a.has_value() && !side_b.has_value()) {
  201. gradient_direction = *side_a;
  202. } else if (side_a.has_value() && side_b.has_value()) {
  203. // Convert two sides to a corner
  204. if (to_underlying(*side_b) < to_underlying(*side_a))
  205. swap(side_a, side_b);
  206. if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Left)
  207. gradient_direction = SideOrCorner::TopLeft;
  208. else if (side_a == SideOrCorner::Top && side_b == SideOrCorner::Right)
  209. gradient_direction = SideOrCorner::TopRight;
  210. else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Left)
  211. gradient_direction = SideOrCorner::BottomLeft;
  212. else if (side_a == SideOrCorner::Bottom && side_b == SideOrCorner::Right)
  213. gradient_direction = SideOrCorner::BottomRight;
  214. else
  215. return nullptr;
  216. } else {
  217. return nullptr;
  218. }
  219. } else {
  220. has_direction_param = false;
  221. }
  222. tokens.skip_whitespace();
  223. if (!tokens.has_next_token())
  224. return nullptr;
  225. if (has_direction_param && !tokens.next_token().is(Token::Type::Comma))
  226. return nullptr;
  227. auto color_stops = parse_linear_color_stop_list(tokens);
  228. if (!color_stops.has_value())
  229. return nullptr;
  230. return LinearGradientStyleValue::create(gradient_direction, move(*color_stops), gradient_type, repeating_gradient);
  231. }
  232. RefPtr<StyleValue> Parser::parse_conic_gradient_function(ComponentValue const& component_value)
  233. {
  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.skip_whitespace();
  245. if (!tokens.has_next_token())
  246. return nullptr;
  247. Angle from_angle(0, Angle::Type::Deg);
  248. PositionValue at_position = PositionValue::center();
  249. // conic-gradient( [ [ from <angle> ]? [ at <position> ]? ] ||
  250. // <color-interpolation-method> , <angular-color-stop-list> )
  251. auto token = tokens.peek_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. (void)tokens.next_token();
  260. tokens.skip_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.next_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(tokens);
  286. if (!position.has_value())
  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.skip_whitespace();
  300. if (!tokens.has_next_token())
  301. return nullptr;
  302. token = tokens.peek_token();
  303. }
  304. tokens.skip_whitespace();
  305. if (!tokens.has_next_token())
  306. return nullptr;
  307. if ((got_from_angle || got_at_position || got_color_interpolation_method) && !tokens.next_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. return ConicGradientStyleValue::create(from_angle, at_position, move(*color_stops), repeating_gradient);
  313. }
  314. RefPtr<StyleValue> Parser::parse_radial_gradient_function(ComponentValue const& component_value)
  315. {
  316. using EndingShape = RadialGradientStyleValue::EndingShape;
  317. using Extent = RadialGradientStyleValue::Extent;
  318. using CircleSize = RadialGradientStyleValue::CircleSize;
  319. using EllipseSize = RadialGradientStyleValue::EllipseSize;
  320. using Size = RadialGradientStyleValue::Size;
  321. if (!component_value.is_function())
  322. return nullptr;
  323. auto repeating_gradient = GradientRepeating::No;
  324. auto function_name = component_value.function().name().bytes_as_string_view();
  325. function_name = consume_if_starts_with(function_name, "repeating-"sv, [&] {
  326. repeating_gradient = GradientRepeating::Yes;
  327. });
  328. if (!function_name.equals_ignoring_ascii_case("radial-gradient"sv))
  329. return nullptr;
  330. TokenStream tokens { component_value.function().values() };
  331. tokens.skip_whitespace();
  332. if (!tokens.has_next_token())
  333. return nullptr;
  334. bool expect_comma = false;
  335. auto commit_value = [&]<typename... T>(auto value, T&... transactions) {
  336. (transactions.commit(), ...);
  337. return value;
  338. };
  339. // radial-gradient( [ <ending-shape> || <size> ]? [ at <position> ]? , <color-stop-list> )
  340. Size size = Extent::FarthestCorner;
  341. EndingShape ending_shape = EndingShape::Circle;
  342. PositionValue at_position = PositionValue::center();
  343. auto parse_ending_shape = [&]() -> Optional<EndingShape> {
  344. auto transaction = tokens.begin_transaction();
  345. tokens.skip_whitespace();
  346. auto& token = tokens.next_token();
  347. if (!token.is(Token::Type::Ident))
  348. return {};
  349. auto ident = token.token().ident();
  350. if (ident.equals_ignoring_ascii_case("circle"sv))
  351. return commit_value(EndingShape::Circle, transaction);
  352. if (ident.equals_ignoring_ascii_case("ellipse"sv))
  353. return commit_value(EndingShape::Ellipse, transaction);
  354. return {};
  355. };
  356. auto parse_extent_keyword = [](StringView keyword) -> Optional<Extent> {
  357. if (keyword.equals_ignoring_ascii_case("closest-corner"sv))
  358. return Extent::ClosestCorner;
  359. if (keyword.equals_ignoring_ascii_case("closest-side"sv))
  360. return Extent::ClosestSide;
  361. if (keyword.equals_ignoring_ascii_case("farthest-corner"sv))
  362. return Extent::FarthestCorner;
  363. if (keyword.equals_ignoring_ascii_case("farthest-side"sv))
  364. return Extent::FarthestSide;
  365. return {};
  366. };
  367. auto parse_size = [&]() -> Optional<Size> {
  368. // <size> =
  369. // <extent-keyword> |
  370. // <length [0,∞]> |
  371. // <length-percentage [0,∞]>{2}
  372. auto transaction_size = tokens.begin_transaction();
  373. tokens.skip_whitespace();
  374. if (!tokens.has_next_token())
  375. return {};
  376. auto& token = tokens.next_token();
  377. if (token.is(Token::Type::Ident)) {
  378. auto extent = parse_extent_keyword(token.token().ident());
  379. if (!extent.has_value())
  380. return {};
  381. return commit_value(*extent, transaction_size);
  382. }
  383. auto first_dimension = parse_dimension(token);
  384. if (!first_dimension.has_value())
  385. return {};
  386. if (!first_dimension->is_length_percentage())
  387. return {};
  388. auto transaction_second_dimension = tokens.begin_transaction();
  389. tokens.skip_whitespace();
  390. if (tokens.has_next_token()) {
  391. auto& second_token = tokens.next_token();
  392. auto second_dimension = parse_dimension(second_token);
  393. if (second_dimension.has_value() && second_dimension->is_length_percentage())
  394. return commit_value(EllipseSize { first_dimension->length_percentage(), second_dimension->length_percentage() },
  395. transaction_size, transaction_second_dimension);
  396. }
  397. if (first_dimension->is_length())
  398. return commit_value(CircleSize { first_dimension->length() }, transaction_size);
  399. return {};
  400. };
  401. {
  402. // [ <ending-shape> || <size> ]?
  403. auto maybe_ending_shape = parse_ending_shape();
  404. auto maybe_size = parse_size();
  405. if (!maybe_ending_shape.has_value() && maybe_size.has_value())
  406. maybe_ending_shape = parse_ending_shape();
  407. if (maybe_size.has_value()) {
  408. size = *maybe_size;
  409. expect_comma = true;
  410. }
  411. if (maybe_ending_shape.has_value()) {
  412. expect_comma = true;
  413. ending_shape = *maybe_ending_shape;
  414. if (ending_shape == EndingShape::Circle && size.has<EllipseSize>())
  415. return nullptr;
  416. if (ending_shape == EndingShape::Ellipse && size.has<CircleSize>())
  417. return nullptr;
  418. } else {
  419. ending_shape = size.has<CircleSize>() ? EndingShape::Circle : EndingShape::Ellipse;
  420. }
  421. }
  422. tokens.skip_whitespace();
  423. if (!tokens.has_next_token())
  424. return nullptr;
  425. auto& token = tokens.peek_token();
  426. if (token.is(Token::Type::Ident) && token.token().ident().equals_ignoring_ascii_case("at"sv)) {
  427. (void)tokens.next_token();
  428. auto position = parse_position(tokens);
  429. if (!position.has_value())
  430. return nullptr;
  431. at_position = *position;
  432. expect_comma = true;
  433. }
  434. tokens.skip_whitespace();
  435. if (!tokens.has_next_token())
  436. return nullptr;
  437. if (expect_comma && !tokens.next_token().is(Token::Type::Comma))
  438. return nullptr;
  439. // <color-stop-list>
  440. auto color_stops = parse_linear_color_stop_list(tokens);
  441. if (!color_stops.has_value())
  442. return nullptr;
  443. return RadialGradientStyleValue::create(ending_shape, size, at_position, move(*color_stops), repeating_gradient);
  444. }
  445. }