SourceSet.cpp 17 KB

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