SourceSet.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. /*
  2. * Copyright (c) 2023, Andreas Kling <kling@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Function.h>
  7. #include <AK/HashMap.h>
  8. #include <AK/QuickSort.h>
  9. #include <LibWeb/Bindings/MainThreadVM.h>
  10. #include <LibWeb/CSS/Parser/Parser.h>
  11. #include <LibWeb/DOM/Document.h>
  12. #include <LibWeb/HTML/SourceSet.h>
  13. #include <LibWeb/Infra/CharacterTypes.h>
  14. #include <LibWeb/Layout/Node.h>
  15. namespace Web::HTML {
  16. SourceSet::SourceSet()
  17. : m_source_size(CSS::Length::make_auto())
  18. {
  19. }
  20. bool SourceSet::is_empty() const
  21. {
  22. return m_sources.is_empty();
  23. }
  24. static double pixel_density(ImageSource const& image_source)
  25. {
  26. return image_source.descriptor.get<ImageSource::PixelDensityDescriptorValue>().value;
  27. }
  28. // https://html.spec.whatwg.org/multipage/images.html#select-an-image-source-from-a-source-set
  29. ImageSourceAndPixelDensity SourceSet::select_an_image_source()
  30. {
  31. // 1. If an entry b in sourceSet has the same associated pixel density descriptor as an earlier entry a in sourceSet,
  32. // then remove entry b.
  33. // Repeat this step until none of the entries in sourceSet have the same associated pixel density descriptor
  34. // as an earlier entry.
  35. Vector<ImageSource> unique_pixel_density_sources;
  36. HashMap<double, ImageSource> unique_pixel_density_sources_map;
  37. for (auto const& source : m_sources) {
  38. auto source_pixel_density = pixel_density(source);
  39. if (!unique_pixel_density_sources_map.contains(source_pixel_density)) {
  40. unique_pixel_density_sources.append(source);
  41. unique_pixel_density_sources_map.set(source_pixel_density, source);
  42. }
  43. }
  44. // 2. In an implementation-defined manner, choose one image source from sourceSet. Let this be selectedSource.
  45. // In our case, select the lowest density greater than 1, otherwise the greatest density available.
  46. // 3. Return selectedSource and its associated pixel density.
  47. quick_sort(unique_pixel_density_sources, [](auto& a, auto& b) {
  48. return pixel_density(a) < pixel_density(b);
  49. });
  50. for (auto const& source : unique_pixel_density_sources) {
  51. if (pixel_density(source) >= 1) {
  52. return { source, pixel_density(source) };
  53. }
  54. }
  55. return { unique_pixel_density_sources.last(), pixel_density(unique_pixel_density_sources.last()) };
  56. }
  57. static StringView collect_a_sequence_of_code_points(Function<bool(u32 code_point)> condition, StringView input, size_t& position)
  58. {
  59. // 1. Let result be the empty string.
  60. // 2. While position doesn’t point past the end of input and the code point at position within input meets the condition condition:
  61. // 1. Append that code point to the end of result.
  62. // 2. Advance position by 1.
  63. // 3. Return result.
  64. size_t start = position;
  65. while (position < input.length() && condition(input[position]))
  66. ++position;
  67. return input.substring_view(start, position - start);
  68. }
  69. // https://html.spec.whatwg.org/multipage/images.html#parse-a-srcset-attribute
  70. SourceSet parse_a_srcset_attribute(StringView input)
  71. {
  72. // 1. Let input be the value passed to this algorithm.
  73. // 2. Let position be a pointer into input, initially pointing at the start of the string.
  74. size_t position = 0;
  75. // 3. Let candidates be an initially empty source set.
  76. SourceSet candidates;
  77. splitting_loop:
  78. // 4. Splitting loop: Collect a sequence of code points that are ASCII whitespace or U+002C COMMA characters from input given position.
  79. // If any U+002C COMMA characters were collected, that is a parse error.
  80. collect_a_sequence_of_code_points(
  81. [](u32 code_point) {
  82. if (code_point == ',') {
  83. // FIXME: Report a parse error somehow.
  84. return true;
  85. }
  86. return Infra::is_ascii_whitespace(code_point);
  87. },
  88. input, position);
  89. // 5. If position is past the end of input, return candidates.
  90. if (position >= input.length()) {
  91. return candidates;
  92. }
  93. // 6. Collect a sequence of code points that are not ASCII whitespace from input given position, and let that be url.
  94. auto url = collect_a_sequence_of_code_points(
  95. [](u32 code_point) { return !Infra::is_ascii_whitespace(code_point); },
  96. input, position);
  97. // 7. Let descriptors be a new empty list.
  98. Vector<String> descriptors;
  99. // 8. If url ends with U+002C (,), then:
  100. if (url.ends_with(',')) {
  101. // 1. Remove all trailing U+002C COMMA characters from url. If this removed more than one character, that is a parse error.
  102. while (url.ends_with(','))
  103. url = url.substring_view(0, url.length() - 1);
  104. }
  105. // Otherwise:
  106. else {
  107. // 1. Descriptor tokenizer: Skip ASCII whitespace within input given position.
  108. collect_a_sequence_of_code_points(
  109. [](u32 code_point) { return Infra::is_ascii_whitespace(code_point); },
  110. input, position);
  111. // 2. Let current descriptor be the empty string.
  112. StringBuilder current_descriptor;
  113. enum class State {
  114. InDescriptor,
  115. InParens,
  116. AfterDescriptor,
  117. };
  118. // 3. Let state be in descriptor.
  119. auto state = State::InDescriptor;
  120. // 4. Let c be the character at position. Do the following depending on the value of state.
  121. // For the purpose of this step, "EOF" is a special character representing that position is past the end of input.
  122. for (;;) {
  123. Optional<u32> c;
  124. if (position < input.length()) {
  125. c = input[position];
  126. }
  127. switch (state) {
  128. // - In descriptor
  129. case State::InDescriptor:
  130. // Do the following, depending on the value of c:
  131. // - ASCII whitespace
  132. if (c.has_value() && Infra::is_ascii_whitespace(c.value())) {
  133. // If current descriptor is not empty, append current descriptor to descriptors and let current descriptor be the empty string.
  134. if (!current_descriptor.is_empty()) {
  135. descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
  136. }
  137. // Set state to after descriptor.
  138. state = State::AfterDescriptor;
  139. }
  140. // U+002C COMMA (,)
  141. else if (c.has_value() && c.value() == ',') {
  142. // Advance position to the next character in input.
  143. position += 1;
  144. // If current descriptor is not empty, append current descriptor to descriptors.
  145. if (!current_descriptor.is_empty()) {
  146. descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
  147. }
  148. // Jump to the step labeled descriptor parser.
  149. goto descriptor_parser;
  150. }
  151. // U+0028 LEFT PARENTHESIS (()
  152. else if (c.has_value() && c.value() == '(') {
  153. // Append c to current descriptor.
  154. current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
  155. // Set state to in parens.
  156. state = State::InParens;
  157. }
  158. // EOF
  159. else if (!c.has_value()) {
  160. // If current descriptor is not empty, append current descriptor to descriptors.
  161. if (!current_descriptor.is_empty()) {
  162. descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
  163. }
  164. // Jump to the step labeled descriptor parser.
  165. goto descriptor_parser;
  166. }
  167. // Anything else
  168. else {
  169. // Append c to current descriptor.
  170. current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
  171. }
  172. break;
  173. // - In parens
  174. case State::InParens:
  175. // Do the following, depending on the value of c:
  176. // U+0029 RIGHT PARENTHESIS ())
  177. if (c.has_value() && c.value() == ')') {
  178. // Append c to current descriptor.
  179. current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
  180. // Set state to in descriptor.
  181. state = State::InDescriptor;
  182. }
  183. // EOF
  184. else if (!c.has_value()) {
  185. // Append current descriptor to descriptors.
  186. descriptors.append(current_descriptor.to_string().release_value_but_fixme_should_propagate_errors());
  187. // Jump to the step labeled descriptor parser.
  188. goto descriptor_parser;
  189. }
  190. // Anything else
  191. else {
  192. // Append c to current descriptor.
  193. current_descriptor.try_append_code_point(c.value()).release_value_but_fixme_should_propagate_errors();
  194. }
  195. break;
  196. // - After descriptor
  197. case State::AfterDescriptor:
  198. // Do the following, depending on the value of c:
  199. // ASCII whitespace
  200. if (c.has_value() && Infra::is_ascii_whitespace(c.value())) {
  201. // Stay in this state.
  202. }
  203. // EOF
  204. else if (!c.has_value()) {
  205. // Jump to the step labeled descriptor parser.
  206. goto descriptor_parser;
  207. }
  208. // Anything else
  209. else {
  210. // Set state to in descriptor.
  211. state = State::InDescriptor;
  212. // Set position to the previous character in input.
  213. position -= 1;
  214. }
  215. break;
  216. }
  217. // Advance position to the next character in input. Repeat this step.
  218. position += 1;
  219. }
  220. }
  221. descriptor_parser:
  222. // 9. Descriptor parser: Let error be no.
  223. bool error = false;
  224. // 10. Let width be absent.
  225. Optional<int> width;
  226. // 11. Let density be absent.
  227. Optional<float> density;
  228. // 12. Let future-compat-h be absent.
  229. Optional<int> future_compat_h;
  230. // 13. For each descriptor in descriptors, run the appropriate set of steps from the following list:
  231. for (auto& descriptor : descriptors) {
  232. auto last_character = descriptor.bytes_as_string_view().bytes().last();
  233. auto descriptor_without_last_character = descriptor.bytes_as_string_view().substring_view(0, descriptor.bytes_as_string_view().length() - 1);
  234. auto as_int = descriptor_without_last_character.to_int<i32>();
  235. auto as_float = descriptor_without_last_character.to_float();
  236. // - If the descriptor consists of a valid non-negative integer followed by a U+0077 LATIN SMALL LETTER W character
  237. if (last_character == 'w' && as_int.has_value()) {
  238. // NOOP: 1. If the user agent does not support the sizes attribute, let error be yes.
  239. // 2. If width and density are not both absent, then let error be yes.
  240. if (width.has_value() || density.has_value()) {
  241. error = true;
  242. }
  243. // FIXME: 3. Apply the rules for parsing non-negative integers to the descriptor.
  244. // If the result is zero, let error be yes. Otherwise, let width be the result.
  245. width = as_int.value();
  246. }
  247. // - If the descriptor consists of a valid floating-point number followed by a U+0078 LATIN SMALL LETTER X character
  248. else if (last_character == 'x' && as_float.has_value()) {
  249. // 1. If width, density and future-compat-h are not all absent, then let error be yes.
  250. if (width.has_value() || density.has_value() || future_compat_h.has_value()) {
  251. error = true;
  252. }
  253. // FIXME: 2. Apply the rules for parsing floating-point number values to the descriptor.
  254. // If the result is less than zero, let error be yes. Otherwise, let density be the result.
  255. density = as_float.value();
  256. }
  257. // - If the descriptor consists of a valid non-negative integer followed by a U+0068 LATIN SMALL LETTER H character
  258. else if (last_character == 'h' && as_int.has_value()) {
  259. // This is a parse error.
  260. // 1. If future-compat-h and density are not both absent, then let error be yes.
  261. if (future_compat_h.has_value() || density.has_value()) {
  262. error = true;
  263. }
  264. // FIXME: 2. Apply the rules for parsing non-negative integers to the descriptor.
  265. // If the result is zero, let error be yes. Otherwise, let future-compat-h be the result.
  266. future_compat_h = as_int.value();
  267. }
  268. // - Anything else
  269. else {
  270. // Let error be yes.
  271. error = true;
  272. }
  273. }
  274. // 14. If future-compat-h is not absent and width is absent, let error be yes.
  275. if (future_compat_h.has_value() && !width.has_value()) {
  276. error = true;
  277. }
  278. // 15. If error is still no, then append a new image source to candidates whose URL is url,
  279. // associated with a width width if not absent and a pixel density density if not absent.
  280. // Otherwise, there is a parse error.
  281. if (!error) {
  282. ImageSource source;
  283. source.url = String::from_utf8(url).release_value_but_fixme_should_propagate_errors();
  284. if (width.has_value())
  285. source.descriptor = ImageSource::WidthDescriptorValue { width.value() };
  286. else if (density.has_value())
  287. source.descriptor = ImageSource::PixelDensityDescriptorValue { density.value() };
  288. candidates.m_sources.append(move(source));
  289. }
  290. // 16. Return to the step labeled splitting loop.
  291. goto splitting_loop;
  292. }
  293. // https://html.spec.whatwg.org/multipage/images.html#parse-a-sizes-attribute
  294. CSS::LengthOrCalculated parse_a_sizes_attribute(DOM::Document const& document, StringView sizes)
  295. {
  296. auto css_parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext { document }, sizes).release_value_but_fixme_should_propagate_errors();
  297. return css_parser.parse_as_sizes_attribute();
  298. }
  299. // https://html.spec.whatwg.org/multipage/images.html#create-a-source-set
  300. SourceSet SourceSet::create(DOM::Element const& element, String default_source, String srcset, String sizes)
  301. {
  302. // 1. Let source set be an empty source set.
  303. SourceSet source_set;
  304. // 2. If srcset is not an empty string, then set source set to the result of parsing srcset.
  305. if (!srcset.is_empty())
  306. source_set = parse_a_srcset_attribute(srcset);
  307. // 3. Let source size be the result of parsing sizes.
  308. source_set.m_source_size = parse_a_sizes_attribute(element.document(), sizes);
  309. // 4. If default source is not the empty string and source set does not contain an image source
  310. // with a pixel density descriptor value of 1, and no image source with a width descriptor,
  311. // append default source to source set.
  312. if (!default_source.is_empty()) {
  313. bool contains_image_source_with_pixel_density_descriptor_value_of_1 = false;
  314. bool contains_image_source_with_width_descriptor = false;
  315. for (auto& source : source_set.m_sources) {
  316. if (source.descriptor.has<ImageSource::PixelDensityDescriptorValue>()) {
  317. if (source.descriptor.get<ImageSource::PixelDensityDescriptorValue>().value == 1.0)
  318. contains_image_source_with_pixel_density_descriptor_value_of_1 = true;
  319. }
  320. if (source.descriptor.has<ImageSource::WidthDescriptorValue>())
  321. contains_image_source_with_width_descriptor = true;
  322. }
  323. if (!contains_image_source_with_pixel_density_descriptor_value_of_1 && !contains_image_source_with_width_descriptor)
  324. source_set.m_sources.append({ .url = default_source, .descriptor = {} });
  325. }
  326. // 5. Normalize the source densities of source set.
  327. source_set.normalize_source_densities(element);
  328. // 6. Return source set.
  329. return source_set;
  330. }
  331. // https://html.spec.whatwg.org/multipage/images.html#normalise-the-source-densities
  332. void SourceSet::normalize_source_densities(DOM::Element const& element)
  333. {
  334. // 1. Let source size be source set's source size.
  335. auto source_size = [&] {
  336. if (!m_source_size.is_calculated()) {
  337. // If the source size is viewport-relative, resolve it against the viewport right now.
  338. if (m_source_size.value().is_viewport_relative()) {
  339. return CSS::Length::make_px(m_source_size.value().viewport_relative_length_to_px(element.document().viewport_rect()));
  340. }
  341. // FIXME: Resolve font-relative lengths against the relevant font size.
  342. return m_source_size.value();
  343. }
  344. // HACK: Flush any pending layouts here so we get an up-to-date length resolution context.
  345. // FIXME: We should have a way to build a LengthResolutionContext for any DOM node without going through the layout tree.
  346. const_cast<DOM::Document&>(element.document()).update_layout();
  347. if (element.layout_node()) {
  348. auto context = CSS::Length::ResolutionContext::for_layout_node(*element.layout_node());
  349. return m_source_size.resolved(context);
  350. }
  351. // FIXME: This is wrong, but we don't have a better way to resolve lengths without a layout node yet.
  352. return CSS::Length::make_auto();
  353. }();
  354. // 2. For each image source in source set:
  355. for (auto& image_source : m_sources) {
  356. // 1. If the image source has a pixel density descriptor, continue to the next image source.
  357. if (image_source.descriptor.has<ImageSource::PixelDensityDescriptorValue>())
  358. continue;
  359. // 2. Otherwise, if the image source has a width descriptor,
  360. // replace the width descriptor with a pixel density descriptor
  361. // with a value of the width descriptor value divided by the source size and a unit of x.
  362. if (image_source.descriptor.has<ImageSource::WidthDescriptorValue>()) {
  363. auto& width_descriptor = image_source.descriptor.get<ImageSource::WidthDescriptorValue>();
  364. if (source_size.is_absolute()) {
  365. image_source.descriptor = ImageSource::PixelDensityDescriptorValue {
  366. .value = (width_descriptor.value / source_size.absolute_length_to_px()).to_double()
  367. };
  368. } else {
  369. dbgln("FIXME: Image element has unresolved relative length '{}' in sizes attribute", source_size);
  370. image_source.descriptor = ImageSource::PixelDensityDescriptorValue {
  371. .value = 1,
  372. };
  373. }
  374. }
  375. // 3. Otherwise, give the image source a pixel density descriptor of 1x.
  376. else {
  377. image_source.descriptor = ImageSource::PixelDensityDescriptorValue {
  378. .value = 1.0f
  379. };
  380. }
  381. }
  382. }
  383. }