GradientParsing.cpp 20 KB

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