OLD | NEW |
1 /* | 1 /* |
2 * Copyright (C) 2013 Apple Inc. All rights reserved. | 2 * Copyright (C) 2013 Apple Inc. All rights reserved. |
3 * Copyright (C) 2013 Google Inc. All rights reserved. | 3 * Copyright (C) 2013 Google Inc. All rights reserved. |
4 * | 4 * |
5 * Redistribution and use in source and binary forms, with or without | 5 * Redistribution and use in source and binary forms, with or without |
6 * modification, are permitted provided that the following conditions are | 6 * modification, are permitted provided that the following conditions are |
7 * met: | 7 * met: |
8 * | 8 * |
9 * * Redistributions of source code must retain the above copyright | 9 * * Redistributions of source code must retain the above copyright |
10 * notice, this list of conditions and the following disclaimer. | 10 * notice, this list of conditions and the following disclaimer. |
(...skipping 20 matching lines...) Expand all Loading... |
31 | 31 |
32 #include "config.h" | 32 #include "config.h" |
33 #include "core/html/parser/HTMLSrcsetParser.h" | 33 #include "core/html/parser/HTMLSrcsetParser.h" |
34 | 34 |
35 #include "RuntimeEnabledFeatures.h" | 35 #include "RuntimeEnabledFeatures.h" |
36 #include "core/html/parser/HTMLParserIdioms.h" | 36 #include "core/html/parser/HTMLParserIdioms.h" |
37 #include "platform/ParsingUtilities.h" | 37 #include "platform/ParsingUtilities.h" |
38 | 38 |
39 namespace WebCore { | 39 namespace WebCore { |
40 | 40 |
41 static bool compareByScaleFactor(const ImageCandidate& first, const ImageCandida
te& second) | 41 static bool compareByDensity(const ImageCandidate& first, const ImageCandidate&
second) |
42 { | 42 { |
43 return first.scaleFactor() < second.scaleFactor(); | 43 return first.density() < second.density(); |
| 44 } |
| 45 |
| 46 enum DescriptorTokenizerState { |
| 47 Start, |
| 48 InParenthesis, |
| 49 AfterToken, |
| 50 }; |
| 51 |
| 52 struct DescriptorToken { |
| 53 unsigned start; |
| 54 unsigned length; |
| 55 |
| 56 DescriptorToken(unsigned start, unsigned length) |
| 57 : start(start) |
| 58 , length(length) |
| 59 { |
| 60 } |
| 61 |
| 62 unsigned lastIndex() |
| 63 { |
| 64 return start + length - 1; |
| 65 } |
| 66 |
| 67 template<typename CharType> |
| 68 int toInt(const CharType* attribute, bool& isValid) |
| 69 { |
| 70 return charactersToInt(attribute + start, length - 1, &isValid); |
| 71 } |
| 72 |
| 73 template<typename CharType> |
| 74 float toFloat(const CharType* attribute, bool& isValid) |
| 75 { |
| 76 return charactersToFloat(attribute + start, length - 1, &isValid); |
| 77 } |
| 78 }; |
| 79 |
| 80 template<typename CharType> |
| 81 static void appendDescriptorAndReset(const CharType* attributeStart, const CharT
ype*& descriptorStart, const CharType* position, Vector<DescriptorToken>& descri
ptors) |
| 82 { |
| 83 if (position > descriptorStart) |
| 84 descriptors.append(DescriptorToken(descriptorStart - attributeStart, pos
ition - descriptorStart)); |
| 85 descriptorStart = 0; |
| 86 } |
| 87 |
| 88 // The following is called appendCharacter to match the spec's terminology. |
| 89 template<typename CharType> |
| 90 static void appendCharacter(const CharType* descriptorStart, const CharType* pos
ition) |
| 91 { |
| 92 // Since we don't copy the tokens, this just set the point where the descrip
tor tokens start. |
| 93 if (!descriptorStart) |
| 94 descriptorStart = position; |
44 } | 95 } |
45 | 96 |
46 template<typename CharType> | 97 template<typename CharType> |
47 inline bool isComma(CharType character) | 98 static bool isEOF(const CharType* position, const CharType* end) |
48 { | 99 { |
49 return character == ','; | 100 return position >= end; |
50 } | 101 } |
51 | 102 |
52 template<typename CharType> | 103 template<typename CharType> |
53 static bool parseDescriptors(const CharType* descriptorsStart, const CharType* d
escriptorsEnd, DescriptorParsingResult& result) | 104 static void tokenizeDescriptors(const CharType* attributeStart, |
| 105 const CharType*& position, |
| 106 const CharType* attributeEnd, |
| 107 Vector<DescriptorToken>& descriptors) |
54 { | 108 { |
55 const CharType* position = descriptorsStart; | 109 DescriptorTokenizerState state = Start; |
56 bool isValid = false; | 110 const CharType* descriptorsStart = position; |
57 bool isEmptyDescriptor = !(descriptorsEnd > descriptorsStart); | 111 const CharType* currentDescriptorStart = descriptorsStart; |
58 while (position < descriptorsEnd) { | 112 while (true) { |
59 // 13.1. Let descriptor list be the result of splitting unparsed descrip
tors on spaces. | 113 switch (state) { |
60 skipWhile<CharType, isHTMLSpace<CharType> >(position, descriptorsEnd); | 114 case Start: |
61 const CharType* currentDescriptorStart = position; | 115 if (isEOF(position, attributeEnd)) { |
62 skipWhile<CharType, isNotHTMLSpace<CharType> >(position, descriptorsEnd)
; | 116 appendDescriptorAndReset(attributeStart, currentDescriptorStart,
attributeEnd, descriptors); |
63 const CharType* currentDescriptorEnd = position; | 117 return; |
| 118 } |
| 119 if (isComma(*position)) { |
| 120 appendDescriptorAndReset(attributeStart, currentDescriptorStart,
position, descriptors); |
| 121 ++position; |
| 122 return; |
| 123 } |
| 124 if (isHTMLSpace(*position)) { |
| 125 appendDescriptorAndReset(attributeStart, currentDescriptorStart,
position, descriptors); |
| 126 currentDescriptorStart = position + 1; |
| 127 state = AfterToken; |
| 128 } else if (*position == '(') { |
| 129 appendCharacter(currentDescriptorStart, position); |
| 130 state = InParenthesis; |
| 131 } else { |
| 132 appendCharacter(currentDescriptorStart, position); |
| 133 } |
| 134 break; |
| 135 case InParenthesis: |
| 136 if (isEOF(position, attributeEnd)) { |
| 137 appendDescriptorAndReset(attributeStart, currentDescriptorStart,
attributeEnd, descriptors); |
| 138 return; |
| 139 } |
| 140 if (*position == ')') { |
| 141 appendCharacter(currentDescriptorStart, position); |
| 142 state = Start; |
| 143 } else { |
| 144 appendCharacter(currentDescriptorStart, position); |
| 145 } |
| 146 break; |
| 147 case AfterToken: |
| 148 if (isEOF(position, attributeEnd)) |
| 149 return; |
| 150 if (!isHTMLSpace(*position)) { |
| 151 state = Start; |
| 152 currentDescriptorStart = position; |
| 153 --position; |
| 154 } |
| 155 break; |
| 156 } |
| 157 ++position; |
| 158 } |
| 159 } |
64 | 160 |
65 ++position; | 161 template<typename CharType> |
66 ASSERT(currentDescriptorEnd > currentDescriptorStart); | 162 static bool parseDescriptors(const CharType* attribute, Vector<DescriptorToken>&
descriptors, DescriptorParsingResult& result) |
67 --currentDescriptorEnd; | 163 { |
68 unsigned descriptorLength = currentDescriptorEnd - currentDescriptorStar
t; | 164 for (Vector<DescriptorToken>::iterator it = descriptors.begin(); it != descr
iptors.end(); ++it) { |
69 if (*currentDescriptorEnd == 'x') { | 165 if (it->length == 0) |
70 if (result.foundDescriptor()) | 166 continue; |
| 167 CharType c = attribute[it->lastIndex()]; |
| 168 bool isValid = false; |
| 169 if (RuntimeEnabledFeatures::pictureSizesEnabled() && c == 'w') { |
| 170 if (result.hasDensity() || result.hasWidth()) |
71 return false; | 171 return false; |
72 result.scaleFactor = charactersToFloat(currentDescriptorStart, descr
iptorLength, &isValid); | 172 int resourceWidth = it->toInt(attribute, isValid); |
73 if (!isValid || result.scaleFactor < 0) | 173 if (!isValid || resourceWidth <= 0) |
74 return false; | 174 return false; |
75 } else if (RuntimeEnabledFeatures::pictureSizesEnabled() && *currentDesc
riptorEnd == 'w') { | 175 result.setResourceWidth(resourceWidth); |
76 if (result.foundDescriptor()) | 176 } else if (RuntimeEnabledFeatures::pictureSizesEnabled() && c == 'h') { |
| 177 // This is here only for future compat purposes. |
| 178 // The value of the 'h' descriptor is not used. |
| 179 if (result.hasDensity() || result.hasHeight()) |
77 return false; | 180 return false; |
78 result.resourceWidth = charactersToInt(currentDescriptorStart, descr
iptorLength, &isValid); | 181 int resourceHeight = it->toInt(attribute, isValid); |
79 if (!isValid || result.resourceWidth <= 0) | 182 if (!isValid || resourceHeight <= 0) |
80 return false; | 183 return false; |
| 184 result.setResourceHeight(resourceHeight); |
| 185 } else if (c == 'x') { |
| 186 if (result.hasDensity() || result.hasHeight() || result.hasWidth()) |
| 187 return false; |
| 188 int density = it->toFloat(attribute, isValid); |
| 189 if (!isValid || density < 0) |
| 190 return false; |
| 191 result.setDensity(density); |
81 } | 192 } |
82 } | 193 } |
83 if (isEmptyDescriptor) | 194 return true; |
84 result.scaleFactor = 1.0; | |
85 return result.foundDescriptor(); | |
86 } | 195 } |
87 | 196 |
88 // http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content-
1.html#processing-the-image-candidates | 197 static bool parseDescriptors(const String& attribute, Vector<DescriptorToken>& d
escriptors, DescriptorParsingResult& result) |
| 198 { |
| 199 // FIXME: See if StringView can't be extended to replace DescriptorToken her
e. |
| 200 if (attribute.is8Bit()) { |
| 201 return parseDescriptors(attribute.characters8(), descriptors, result); |
| 202 } |
| 203 return parseDescriptors(attribute.characters16(), descriptors, result); |
| 204 } |
| 205 |
| 206 // http://picture.responsiveimages.org/#parse-srcset-attr |
89 template<typename CharType> | 207 template<typename CharType> |
90 static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, con
st CharType* attributeStart, unsigned length, Vector<ImageCandidate>& imageCandi
dates) | 208 static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, con
st CharType* attributeStart, unsigned length, Vector<ImageCandidate>& imageCandi
dates) |
91 { | 209 { |
92 const CharType* position = attributeStart; | 210 const CharType* position = attributeStart; |
93 const CharType* attributeEnd = position + length; | 211 const CharType* attributeEnd = position + length; |
94 | 212 |
95 while (position < attributeEnd) { | 213 while (position < attributeEnd) { |
96 DescriptorParsingResult result; | 214 // 4. Splitting loop: Collect a sequence of characters that are space ch
aracters or U+002C COMMA characters. |
97 // 4. Splitting loop: Skip whitespace. | 215 skipWhile<CharType, isHTMLSpaceOrComma<CharType> >(position, attributeEn
d); |
98 skipWhile<CharType, isHTMLSpace<CharType> >(position, attributeEnd); | 216 if (position == attributeEnd) { |
99 if (position == attributeEnd) | 217 // Contrary to spec language - descriptor parsing happens on each ca
ndidate, so when we reach the attributeEnd, we can exit. |
100 break; | 218 break; |
| 219 } |
101 const CharType* imageURLStart = position; | 220 const CharType* imageURLStart = position; |
| 221 // 6. Collect a sequence of characters that are not space characters, an
d let that be url. |
102 | 222 |
103 // If The current candidate is either totally empty or only contains spa
ce, skipping. | |
104 if (*position == ',') { | |
105 ++position; | |
106 continue; | |
107 } | |
108 | |
109 // 5. Collect a sequence of characters that are not space characters, an
d let that be url. | |
110 skipUntil<CharType, isHTMLSpace<CharType> >(position, attributeEnd); | 223 skipUntil<CharType, isHTMLSpace<CharType> >(position, attributeEnd); |
111 const CharType* imageURLEnd = position; | 224 const CharType* imageURLEnd = position; |
112 | 225 |
113 if (position != attributeEnd && *(position - 1) == ',') { | 226 DescriptorParsingResult result; |
114 --imageURLEnd; | 227 |
115 result.scaleFactor = 1.0; | 228 // 8. If url ends with a U+002C COMMA character (,) |
| 229 if (isComma(*(position - 1))) { |
| 230 // Remove all trailing U+002C COMMA characters from url. |
| 231 imageURLEnd = position - 1; |
| 232 reverseSkipWhile<CharType, isComma>(imageURLEnd, imageURLStart); |
| 233 ++imageURLEnd; |
| 234 // If url is empty, then jump to the step labeled splitting loop. |
| 235 if (imageURLStart == imageURLEnd) |
| 236 continue; |
116 } else { | 237 } else { |
117 // 7. Collect a sequence of characters that are not "," (U+002C) cha
racters, and let that be descriptors. | 238 // Advancing position here (contrary to spec) to avoid an useless ex
tra state machine step. |
118 skipWhile<CharType, isHTMLSpace<CharType> >(position, attributeEnd); | 239 // Filed a spec bug: https://github.com/ResponsiveImagesCG/picture-e
lement/issues/189 |
119 const CharType* descriptorsStart = position; | 240 ++position; |
120 skipUntil<CharType, isComma<CharType> >(position, attributeEnd); | 241 Vector<DescriptorToken> descriptorTokens; |
121 const CharType* descriptorsEnd = position; | 242 tokenizeDescriptors(attributeStart, position, attributeEnd, descript
orTokens); |
122 if (!parseDescriptors(descriptorsStart, descriptorsEnd, result)) | 243 // Contrary to spec language - descriptor parsing happens on each ca
ndidate. |
| 244 // This is a black-box equivalent, to avoid storing descriptor lists
for each candidate. |
| 245 if (!parseDescriptors(attribute, descriptorTokens, result)) |
123 continue; | 246 continue; |
124 } | 247 } |
125 | 248 |
126 ASSERT(imageURLEnd > attributeStart); | 249 ASSERT(imageURLEnd > attributeStart); |
127 unsigned imageURLStartingPosition = imageURLStart - attributeStart; | 250 unsigned imageURLStartingPosition = imageURLStart - attributeStart; |
128 ASSERT(imageURLEnd > imageURLStart); | 251 ASSERT(imageURLEnd > imageURLStart); |
129 unsigned imageURLLength = imageURLEnd - imageURLStart; | 252 unsigned imageURLLength = imageURLEnd - imageURLStart; |
130 imageCandidates.append(ImageCandidate(attribute, imageURLStartingPositio
n, imageURLLength, result, ImageCandidate::SrcsetOrigin)); | 253 imageCandidates.append(ImageCandidate(attribute, imageURLStartingPositio
n, imageURLLength, result, ImageCandidate::SrcsetOrigin)); |
131 // 11. Return to the step labeled splitting loop. | 254 // 11. Return to the step labeled splitting loop. |
132 } | 255 } |
133 } | 256 } |
134 | 257 |
135 static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, Vec
tor<ImageCandidate>& imageCandidates) | 258 static void parseImageCandidatesFromSrcsetAttribute(const String& attribute, Vec
tor<ImageCandidate>& imageCandidates) |
136 { | 259 { |
137 if (attribute.isNull()) | 260 if (attribute.isNull()) |
138 return; | 261 return; |
139 | 262 |
140 if (attribute.is8Bit()) | 263 if (attribute.is8Bit()) |
141 parseImageCandidatesFromSrcsetAttribute<LChar>(attribute, attribute.char
acters8(), attribute.length(), imageCandidates); | 264 parseImageCandidatesFromSrcsetAttribute<LChar>(attribute, attribute.char
acters8(), attribute.length(), imageCandidates); |
142 else | 265 else |
143 parseImageCandidatesFromSrcsetAttribute<UChar>(attribute, attribute.char
acters16(), attribute.length(), imageCandidates); | 266 parseImageCandidatesFromSrcsetAttribute<UChar>(attribute, attribute.char
acters16(), attribute.length(), imageCandidates); |
144 } | 267 } |
145 | 268 |
146 static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, unsigned s
ourceSize, Vector<ImageCandidate>& imageCandidates) | 269 static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, unsigned s
ourceSize, Vector<ImageCandidate>& imageCandidates) |
147 { | 270 { |
| 271 const float defaultDensityValue = 1.0; |
148 bool ignoreSrc = false; | 272 bool ignoreSrc = false; |
149 if (imageCandidates.isEmpty()) | 273 if (imageCandidates.isEmpty()) |
150 return ImageCandidate(); | 274 return ImageCandidate(); |
151 | 275 |
152 // http://picture.responsiveimages.org/#normalize-source-densities | 276 // http://picture.responsiveimages.org/#normalize-source-densities |
153 for (Vector<ImageCandidate>::iterator it = imageCandidates.begin(); it != im
ageCandidates.end(); ++it) { | 277 for (Vector<ImageCandidate>::iterator it = imageCandidates.begin(); it != im
ageCandidates.end(); ++it) { |
154 if (it->resourceWidth() > 0) { | 278 if (it->resourceWidth() > 0) { |
155 it->setScaleFactor((float)it->resourceWidth() / (float)sourceSize); | 279 it->setDensity((float)it->resourceWidth() / (float)sourceSize); |
156 ignoreSrc = true; | 280 ignoreSrc = true; |
| 281 } else if (it->density() < 0) { |
| 282 it->setDensity(defaultDensityValue); |
157 } | 283 } |
158 } | 284 } |
159 | 285 |
160 std::stable_sort(imageCandidates.begin(), imageCandidates.end(), compareBySc
aleFactor); | 286 std::stable_sort(imageCandidates.begin(), imageCandidates.end(), compareByDe
nsity); |
161 | 287 |
162 unsigned i; | 288 unsigned i; |
163 for (i = 0; i < imageCandidates.size() - 1; ++i) { | 289 for (i = 0; i < imageCandidates.size() - 1; ++i) { |
164 if ((imageCandidates[i].scaleFactor() >= deviceScaleFactor) && (!ignoreS
rc || !imageCandidates[i].srcOrigin())) | 290 if ((imageCandidates[i].density() >= deviceScaleFactor) && (!ignoreSrc |
| !imageCandidates[i].srcOrigin())) |
165 break; | 291 break; |
166 } | 292 } |
167 | 293 |
168 if (imageCandidates[i].srcOrigin() && ignoreSrc) { | 294 if (imageCandidates[i].srcOrigin() && ignoreSrc) { |
169 ASSERT(i > 0); | 295 ASSERT(i > 0); |
170 --i; | 296 --i; |
171 } | 297 } |
172 float winningScaleFactor = imageCandidates[i].scaleFactor(); | 298 float winningDensity = imageCandidates[i].density(); |
173 | 299 |
174 unsigned winner = i; | 300 unsigned winner = i; |
175 // 16. If an entry b in candidates has the same associated ... pixel density
as an earlier entry a in candidates, | 301 // 16. If an entry b in candidates has the same associated ... pixel density
as an earlier entry a in candidates, |
176 // then remove entry b | 302 // then remove entry b |
177 while ((i > 0) && (imageCandidates[--i].scaleFactor() == winningScaleFactor)
) | 303 while ((i > 0) && (imageCandidates[--i].density() == winningDensity)) |
178 winner = i; | 304 winner = i; |
179 | 305 |
180 return imageCandidates[winner]; | 306 return imageCandidates[winner]; |
181 } | 307 } |
182 | 308 |
183 ImageCandidate bestFitSourceForSrcsetAttribute(float deviceScaleFactor, unsigned
sourceSize, const String& srcsetAttribute) | 309 ImageCandidate bestFitSourceForSrcsetAttribute(float deviceScaleFactor, unsigned
sourceSize, const String& srcsetAttribute) |
184 { | 310 { |
185 Vector<ImageCandidate> imageCandidates; | 311 Vector<ImageCandidate> imageCandidates; |
186 | 312 |
187 parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates); | 313 parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates); |
188 | 314 |
189 return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates
); | 315 return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates
); |
190 } | 316 } |
191 | 317 |
192 ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, unsigned
sourceSize, const String& srcAttribute, const String& srcsetAttribute) | 318 ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, unsigned
sourceSize, const String& srcAttribute, const String& srcsetAttribute) |
193 { | 319 { |
194 DescriptorParsingResult defaultResult; | |
195 defaultResult.scaleFactor = 1.0; | |
196 | |
197 if (srcsetAttribute.isNull()) { | 320 if (srcsetAttribute.isNull()) { |
198 if (srcAttribute.isNull()) | 321 if (srcAttribute.isNull()) |
199 return ImageCandidate(); | 322 return ImageCandidate(); |
200 return ImageCandidate(srcAttribute, 0, srcAttribute.length(), defaultRes
ult, ImageCandidate::SrcOrigin); | 323 return ImageCandidate(srcAttribute, 0, srcAttribute.length(), Descriptor
ParsingResult(), ImageCandidate::SrcOrigin); |
201 } | 324 } |
202 | 325 |
203 Vector<ImageCandidate> imageCandidates; | 326 Vector<ImageCandidate> imageCandidates; |
204 | 327 |
205 parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates); | 328 parseImageCandidatesFromSrcsetAttribute(srcsetAttribute, imageCandidates); |
206 | 329 |
207 if (!srcAttribute.isEmpty()) | 330 if (!srcAttribute.isEmpty()) |
208 imageCandidates.append(ImageCandidate(srcAttribute, 0, srcAttribute.leng
th(), defaultResult, ImageCandidate::SrcOrigin)); | 331 imageCandidates.append(ImageCandidate(srcAttribute, 0, srcAttribute.leng
th(), DescriptorParsingResult(), ImageCandidate::SrcOrigin)); |
209 | 332 |
210 return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates
); | 333 return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates
); |
211 } | 334 } |
212 | 335 |
213 String bestFitSourceForImageAttributes(float deviceScaleFactor, unsigned sourceS
ize, const String& srcAttribute, ImageCandidate& srcsetImageCandidate) | 336 String bestFitSourceForImageAttributes(float deviceScaleFactor, unsigned sourceS
ize, const String& srcAttribute, ImageCandidate& srcsetImageCandidate) |
214 { | 337 { |
215 DescriptorParsingResult defaultResult; | |
216 defaultResult.scaleFactor = 1.0; | |
217 | |
218 if (srcsetImageCandidate.isEmpty()) | 338 if (srcsetImageCandidate.isEmpty()) |
219 return srcAttribute; | 339 return srcAttribute; |
220 | 340 |
221 Vector<ImageCandidate> imageCandidates; | 341 Vector<ImageCandidate> imageCandidates; |
222 imageCandidates.append(srcsetImageCandidate); | 342 imageCandidates.append(srcsetImageCandidate); |
223 | 343 |
224 if (!srcAttribute.isEmpty()) | 344 if (!srcAttribute.isEmpty()) |
225 imageCandidates.append(ImageCandidate(srcAttribute, 0, srcAttribute.leng
th(), defaultResult, ImageCandidate::SrcOrigin)); | 345 imageCandidates.append(ImageCandidate(srcAttribute, 0, srcAttribute.leng
th(), DescriptorParsingResult(), ImageCandidate::SrcOrigin)); |
226 | 346 |
227 return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates
).toString(); | 347 return pickBestImageCandidate(deviceScaleFactor, sourceSize, imageCandidates
).toString(); |
228 } | 348 } |
229 | 349 |
230 } | 350 } |
OLD | NEW |