You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

css_value.cxx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /*-
  2. * Copyright 2021 Vsevolod Stakhov
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #include "css_value.hxx"
  17. #include "css_colors_list.hxx"
  18. #include "frozen/unordered_map.h"
  19. #include "frozen/string.h"
  20. #include "libutil/util.h"
  21. #include "contrib/robin-hood/robin_hood.h"
  22. #include "fmt/core.h"
  23. #define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
  24. #include "doctest/doctest.h"
  25. /* Helper for unit test stringification */
  26. namespace doctest {
  27. template<> struct StringMaker<rspamd::css::css_color> {
  28. static String convert(const rspamd::css::css_color& value) {
  29. return fmt::format("r={};g={};b={};alpha={}",
  30. value.r, value.g, value.b, value.alpha).c_str();
  31. }
  32. };
  33. }
  34. namespace rspamd::css {
  35. auto css_value::maybe_color_from_string(const std::string_view &input)
  36. -> std::optional<css_value> {
  37. auto found_it = css_colors_map.find(input);
  38. if (found_it != css_colors_map.end()) {
  39. return css_value{found_it->second};
  40. }
  41. return std::nullopt;
  42. }
  43. constexpr static inline auto hexpair_decode(char c1, char c2) -> std::uint8_t {
  44. std::uint8_t ret = 0;
  45. if (c1 >= '0' && c1 <= '9') ret = c1 - '0';
  46. else if (c1 >= 'A' && c1 <= 'F') ret = c1 - 'A' + 10;
  47. else if (c1 >= 'a' && c1 <= 'f') ret = c1 - 'a' + 10;
  48. ret *= 16;
  49. if (c2 >= '0' && c2 <= '9') ret += c2 - '0';
  50. else if (c2 >= 'A' && c2 <= 'F') ret += c2 - 'A' + 10;
  51. else if (c2 >= 'a' && c2 <= 'f') ret += c2 - 'a' + 10;
  52. return ret;
  53. }
  54. auto css_value::maybe_color_from_hex(const std::string_view &input)
  55. -> std::optional<css_value> {
  56. if (input.length() == 6) {
  57. /* Plain RGB */
  58. css_color col(hexpair_decode(input[0], input[1]),
  59. hexpair_decode(input[2], input[3]),
  60. hexpair_decode(input[4], input[5]));
  61. return css_value(col);
  62. }
  63. else if (input.length() == 3) {
  64. /* Rgb as 3 hex digests */
  65. css_color col(hexpair_decode(input[0], input[0]),
  66. hexpair_decode(input[1], input[1]),
  67. hexpair_decode(input[2], input[2]));
  68. return css_value(col);
  69. }
  70. else if (input.length() == 8) {
  71. /* RGBA */
  72. css_color col(hexpair_decode(input[0], input[1]),
  73. hexpair_decode(input[2], input[3]),
  74. hexpair_decode(input[4], input[5]),
  75. hexpair_decode(input[6], input[7]));
  76. return css_value(col);
  77. }
  78. return std::nullopt;
  79. }
  80. constexpr static inline auto rgb_color_component_convert(const css_parser_token &tok)
  81. -> std::uint8_t {
  82. std::uint8_t ret = 0;
  83. if (tok.type == css_parser_token::token_type::number_token) {
  84. auto dbl = std::get<double>(tok.value);
  85. if (tok.flags & css_parser_token::number_percent) {
  86. if (dbl > 100) {
  87. dbl = 100;
  88. }
  89. else if (dbl < 0) {
  90. dbl = 0;
  91. }
  92. ret = (std::uint8_t) (dbl / 100.0 * 255.0);
  93. }
  94. else {
  95. if (dbl > 1) {
  96. dbl = 1;
  97. }
  98. else if (dbl < 0) {
  99. dbl = 0;
  100. }
  101. ret = (std::uint8_t) (dbl * 255.0);
  102. }
  103. }
  104. return ret;
  105. }
  106. constexpr static inline auto alpha_component_convert(const css_parser_token &tok)
  107. -> std::uint8_t {
  108. double ret = 1.0;
  109. if (tok.type == css_parser_token::token_type::number_token) {
  110. auto dbl = std::get<double>(tok.value);
  111. if (tok.flags & css_parser_token::number_percent) {
  112. if (dbl > 100) {
  113. dbl = 100;
  114. }
  115. else if (dbl < 0) {
  116. dbl = 0;
  117. }
  118. ret = (dbl / 100.0);
  119. }
  120. else {
  121. if (dbl > 255) {
  122. dbl = 255;
  123. }
  124. else if (dbl < 0) {
  125. dbl = 0;
  126. }
  127. ret = dbl / 255.0;
  128. }
  129. }
  130. return (std::uint8_t) (ret * 255.0);
  131. }
  132. constexpr static inline auto h_component_convert(const css_parser_token &tok)
  133. -> double {
  134. double ret = 0.0;
  135. if (tok.type == css_parser_token::token_type::number_token) {
  136. auto dbl = std::get<double>(tok.value);
  137. if (tok.flags & css_parser_token::number_percent) {
  138. if (dbl > 100) {
  139. dbl = 100;
  140. }
  141. else if (dbl < 0) {
  142. dbl = 0;
  143. }
  144. ret = (dbl / 100.0);
  145. }
  146. else {
  147. dbl = ((((int) dbl % 360) + 360) % 360); /* Deal with rotations */
  148. ret = dbl / 360.0; /* Normalize to 0..1 */
  149. }
  150. }
  151. return ret;
  152. }
  153. constexpr static inline auto sl_component_convert(const css_parser_token &tok)
  154. -> double {
  155. double ret = 0.0;
  156. if (tok.type == css_parser_token::token_type::number_token) {
  157. ret = tok.get_normal_number_or_default(ret);
  158. }
  159. return ret;
  160. }
  161. static inline auto hsl_to_rgb(double h, double s, double l)
  162. -> css_color {
  163. css_color ret;
  164. constexpr auto hue2rgb = [](auto p, auto q, auto t) -> auto {
  165. if (t < 0.0) {
  166. t += 1.0;
  167. }
  168. if (t > 1.0) {
  169. t -= 1.0;
  170. }
  171. if (t * 6. < 1.0) {
  172. return p + (q - p) * 6.0 * t;
  173. }
  174. if (t * 2. < 1) {
  175. return q;
  176. }
  177. if (t * 3. < 2.) {
  178. return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
  179. }
  180. return p;
  181. };
  182. if (s == 0) {
  183. /* Achromatic */
  184. ret.r = l;
  185. ret.g = l;
  186. ret.b = l;
  187. }
  188. else {
  189. auto q = l <= 0.5 ? l * (1.0 + s) : l + s - l * s;
  190. auto p = 2.0 * l - q;
  191. ret.r = (std::uint8_t) (hue2rgb(p, q, h + 1.0 / 3.0) * 255);
  192. ret.g = (std::uint8_t) (hue2rgb(p, q, h) * 255);
  193. ret.b = (std::uint8_t) (hue2rgb(p, q, h - 1.0 / 3.0) * 255);
  194. }
  195. ret.alpha = 255;
  196. return ret;
  197. }
  198. auto css_value::maybe_color_from_function(const css_consumed_block::css_function_block &func)
  199. -> std::optional<css_value> {
  200. if (func.as_string() == "rgb" && func.args.size() == 3) {
  201. css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()),
  202. rgb_color_component_convert(func.args[1]->get_token_or_empty()),
  203. rgb_color_component_convert(func.args[2]->get_token_or_empty())};
  204. return css_value(col);
  205. }
  206. else if (func.as_string() == "rgba" && func.args.size() == 4) {
  207. css_color col{rgb_color_component_convert(func.args[0]->get_token_or_empty()),
  208. rgb_color_component_convert(func.args[1]->get_token_or_empty()),
  209. rgb_color_component_convert(func.args[2]->get_token_or_empty()),
  210. alpha_component_convert(func.args[3]->get_token_or_empty())};
  211. return css_value(col);
  212. }
  213. else if (func.as_string() == "hsl" && func.args.size() == 3) {
  214. auto h = h_component_convert(func.args[0]->get_token_or_empty());
  215. auto s = sl_component_convert(func.args[1]->get_token_or_empty());
  216. auto l = sl_component_convert(func.args[2]->get_token_or_empty());
  217. auto col = hsl_to_rgb(h, s, l);
  218. return css_value(col);
  219. }
  220. else if (func.as_string() == "hsla" && func.args.size() == 4) {
  221. auto h = h_component_convert(func.args[0]->get_token_or_empty());
  222. auto s = sl_component_convert(func.args[1]->get_token_or_empty());
  223. auto l = sl_component_convert(func.args[2]->get_token_or_empty());
  224. auto col = hsl_to_rgb(h, s, l);
  225. col.alpha = alpha_component_convert(func.args[3]->get_token_or_empty());
  226. return css_value(col);
  227. }
  228. return std::nullopt;
  229. }
  230. auto css_value::maybe_dimension_from_number(const css_parser_token &tok)
  231. -> std::optional<css_value> {
  232. if (std::holds_alternative<double>(tok.value)) {
  233. auto dbl = std::get<double>(tok.value);
  234. css_dimension dim;
  235. dim.dim = dbl;
  236. if (tok.flags & css_parser_token::number_percent) {
  237. dim.is_percent = true;
  238. }
  239. else {
  240. dim.is_percent = false;
  241. }
  242. return css_value{dim};
  243. }
  244. return std::nullopt;
  245. }
  246. constexpr const auto display_names_map = frozen::make_unordered_map<frozen::string, css_display_value>({
  247. {"hidden", css_display_value::DISPLAY_HIDDEN},
  248. {"none", css_display_value::DISPLAY_HIDDEN},
  249. {"inline", css_display_value::DISPLAY_NORMAL},
  250. {"block", css_display_value::DISPLAY_NORMAL},
  251. {"content", css_display_value::DISPLAY_NORMAL},
  252. {"flex", css_display_value::DISPLAY_NORMAL},
  253. {"grid", css_display_value::DISPLAY_NORMAL},
  254. {"inline-block", css_display_value::DISPLAY_NORMAL},
  255. {"inline-flex", css_display_value::DISPLAY_NORMAL},
  256. {"inline-grid", css_display_value::DISPLAY_NORMAL},
  257. {"inline-table", css_display_value::DISPLAY_NORMAL},
  258. {"list-item", css_display_value::DISPLAY_NORMAL},
  259. {"run-in", css_display_value::DISPLAY_NORMAL},
  260. {"table", css_display_value::DISPLAY_NORMAL},
  261. {"table-caption", css_display_value::DISPLAY_NORMAL},
  262. {"table-column-group", css_display_value::DISPLAY_NORMAL},
  263. {"table-header-group", css_display_value::DISPLAY_NORMAL},
  264. {"table-footer-group", css_display_value::DISPLAY_NORMAL},
  265. {"table-row-group", css_display_value::DISPLAY_NORMAL},
  266. {"table-cell", css_display_value::DISPLAY_NORMAL},
  267. {"table-column", css_display_value::DISPLAY_NORMAL},
  268. {"table-row", css_display_value::DISPLAY_NORMAL},
  269. {"initial", css_display_value::DISPLAY_NORMAL},
  270. });
  271. auto css_value::maybe_display_from_string(const std::string_view &input)
  272. -> std::optional<css_value> {
  273. auto f = display_names_map.find(input);
  274. if (f != display_names_map.end()) {
  275. return css_value{f->second};
  276. }
  277. return std::nullopt;
  278. }
  279. auto css_value::debug_str() const -> std::string {
  280. std::string ret;
  281. std::visit([&](const auto &arg) {
  282. using T = std::decay_t<decltype(arg)>;
  283. if constexpr (std::is_same_v<T, css_color>) {
  284. ret += fmt::format("color: r={};g={};b={};alpha={}",
  285. arg.r, arg.g, arg.b, arg.alpha);
  286. }
  287. else if constexpr (std::is_same_v<T, double>) {
  288. ret += "size: " + std::to_string(arg);
  289. }
  290. else if constexpr (std::is_same_v<T, css_dimension>) {
  291. ret += "dimension: " + std::to_string(arg.dim);
  292. if (arg.is_percent) {
  293. ret += "%";
  294. }
  295. }
  296. else if constexpr (std::is_same_v<T, css_display_value>) {
  297. ret += "display: ";
  298. ret += (arg == css_display_value::DISPLAY_HIDDEN ? "hidden" : "normal");
  299. }
  300. else if constexpr (std::is_integral_v<T>) {
  301. ret += "integral: " + std::to_string(static_cast<int>(arg));
  302. }
  303. else {
  304. ret += "nyi";
  305. }
  306. }, value);
  307. return ret;
  308. }
  309. TEST_SUITE("css values") {
  310. TEST_CASE("css hex colors") {
  311. const std::pair<const char*, css_color> hex_tests[] = {
  312. {"000", css_color(0, 0, 0)},
  313. {"000000", css_color(0, 0, 0)},
  314. {"f00", css_color(255, 0, 0)},
  315. {"FEDCBA", css_color(254, 220, 186)},
  316. {"234", css_color(34, 51, 68)},
  317. };
  318. for (const auto &p : hex_tests) {
  319. auto col_parsed = css_value::maybe_color_from_hex(p.first);
  320. //CHECK_UNARY(col_parsed);
  321. //CHECK_UNARY(col_parsed.value().to_color());
  322. auto final_col = col_parsed.value().to_color().value();
  323. CHECK(final_col == p.second);
  324. }
  325. }
  326. TEST_CASE("css colors strings") {
  327. auto passed = 0;
  328. for (const auto &p : css_colors_map) {
  329. /* Match some of the colors selected randomly */
  330. if (rspamd_random_double_fast() > 0.9) {
  331. auto col_parsed = css_value::maybe_color_from_string(p.first);
  332. auto final_col = col_parsed.value().to_color().value();
  333. CHECK_MESSAGE(final_col == p.second, p.first.data());
  334. passed ++;
  335. if (passed > 20) {
  336. break;
  337. }
  338. }
  339. }
  340. }
  341. };
  342. }