Serenity Operating System
at master 415 lines 12 kB view raw
1/* 2 * Copyright (c) 2020, Matthew Olsson <mattco@serenityos.org> 3 * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "AttributeParser.h" 9#include <AK/FloatingPointStringConversions.h> 10#include <AK/StringBuilder.h> 11#include <ctype.h> 12 13namespace Web::SVG { 14 15AttributeParser::AttributeParser(StringView source) 16 : m_source(move(source)) 17{ 18} 19 20Vector<PathInstruction> AttributeParser::parse_path_data(StringView input) 21{ 22 AttributeParser parser { input }; 23 parser.parse_whitespace(); 24 while (!parser.done()) 25 parser.parse_drawto(); 26 if (!parser.m_instructions.is_empty() && parser.m_instructions[0].type != PathInstructionType::Move) { 27 // Invalid. "A path data segment (if there is one) must begin with a "moveto" command." 28 return {}; 29 } 30 return parser.m_instructions; 31} 32 33Optional<float> AttributeParser::parse_coordinate(StringView input) 34{ 35 AttributeParser parser { input }; 36 parser.parse_whitespace(); 37 if (parser.match_coordinate()) { 38 float result = parser.parse_coordinate(); 39 parser.parse_whitespace(); 40 if (parser.done()) 41 return result; 42 } 43 44 return {}; 45} 46 47Optional<float> AttributeParser::parse_length(StringView input) 48{ 49 AttributeParser parser { input }; 50 parser.parse_whitespace(); 51 if (parser.match_coordinate()) { 52 float result = parser.parse_length(); 53 parser.parse_whitespace(); 54 if (parser.done()) 55 return result; 56 } 57 58 return {}; 59} 60 61Optional<float> AttributeParser::parse_positive_length(StringView input) 62{ 63 // FIXME: Where this is used, the spec usually (always?) says "A negative value is an error (see Error processing)." 64 // So, implement error processing! Maybe this should return ErrorOr. 65 auto result = parse_length(input); 66 if (result.has_value() && result.value() < 0) 67 result.clear(); 68 return result; 69} 70 71Vector<Gfx::FloatPoint> AttributeParser::parse_points(StringView input) 72{ 73 AttributeParser parser { input }; 74 75 parser.parse_whitespace(); 76 77 // FIXME: "If an odd number of coordinates is provided, then the element is in error, with the same user agent behavior 78 // as occurs with an incorrectly specified ‘path’ element. In such error cases the user agent will drop the last, 79 // odd coordinate and otherwise render the shape." 80 // The parser currently doesn't notice that there is a missing coordinate, so make it notice! 81 auto coordinate_pair_sequence = parser.parse_coordinate_pair_sequence(); 82 83 parser.parse_whitespace(); 84 if (!parser.done()) 85 return {}; 86 87 // FIXME: This is awkward. Can we return Gfx::FloatPoints from some of these parsing methods instead of Vector<float>? 88 Vector<Gfx::FloatPoint> points; 89 points.ensure_capacity(coordinate_pair_sequence.size()); 90 91 for (auto const& pair : coordinate_pair_sequence) 92 points.empend(pair[0], pair[1]); 93 94 return points; 95} 96 97void AttributeParser::parse_drawto() 98{ 99 if (match('M') || match('m')) { 100 parse_moveto(); 101 } else if (match('Z') || match('z')) { 102 parse_closepath(); 103 } else if (match('L') || match('l')) { 104 parse_lineto(); 105 } else if (match('H') || match('h')) { 106 parse_horizontal_lineto(); 107 } else if (match('V') || match('v')) { 108 parse_vertical_lineto(); 109 } else if (match('C') || match('c')) { 110 parse_curveto(); 111 } else if (match('S') || match('s')) { 112 parse_smooth_curveto(); 113 } else if (match('Q') || match('q')) { 114 parse_quadratic_bezier_curveto(); 115 } else if (match('T') || match('t')) { 116 parse_smooth_quadratic_bezier_curveto(); 117 } else if (match('A') || match('a')) { 118 parse_elliptical_arc(); 119 } else { 120 dbgln("AttributeParser::parse_drawto failed to match: '{}'", ch()); 121 TODO(); 122 } 123} 124 125void AttributeParser::parse_moveto() 126{ 127 bool absolute = consume() == 'M'; 128 parse_whitespace(); 129 for (auto& pair : parse_coordinate_pair_sequence()) 130 m_instructions.append({ PathInstructionType::Move, absolute, pair }); 131} 132 133void AttributeParser::parse_closepath() 134{ 135 bool absolute = consume() == 'Z'; 136 parse_whitespace(); 137 m_instructions.append({ PathInstructionType::ClosePath, absolute, {} }); 138} 139 140void AttributeParser::parse_lineto() 141{ 142 bool absolute = consume() == 'L'; 143 parse_whitespace(); 144 for (auto& pair : parse_coordinate_pair_sequence()) 145 m_instructions.append({ PathInstructionType::Line, absolute, pair }); 146} 147 148void AttributeParser::parse_horizontal_lineto() 149{ 150 bool absolute = consume() == 'H'; 151 parse_whitespace(); 152 m_instructions.append({ PathInstructionType::HorizontalLine, absolute, parse_coordinate_sequence() }); 153} 154 155void AttributeParser::parse_vertical_lineto() 156{ 157 bool absolute = consume() == 'V'; 158 parse_whitespace(); 159 m_instructions.append({ PathInstructionType::VerticalLine, absolute, parse_coordinate_sequence() }); 160} 161 162void AttributeParser::parse_curveto() 163{ 164 bool absolute = consume() == 'C'; 165 parse_whitespace(); 166 167 while (true) { 168 m_instructions.append({ PathInstructionType::Curve, absolute, parse_coordinate_pair_triplet() }); 169 if (match_comma_whitespace()) 170 parse_comma_whitespace(); 171 if (!match_coordinate()) 172 break; 173 } 174} 175 176void AttributeParser::parse_smooth_curveto() 177{ 178 bool absolute = consume() == 'S'; 179 parse_whitespace(); 180 181 while (true) { 182 m_instructions.append({ PathInstructionType::SmoothCurve, absolute, parse_coordinate_pair_double() }); 183 if (match_comma_whitespace()) 184 parse_comma_whitespace(); 185 if (!match_coordinate()) 186 break; 187 } 188} 189 190void AttributeParser::parse_quadratic_bezier_curveto() 191{ 192 bool absolute = consume() == 'Q'; 193 parse_whitespace(); 194 195 while (true) { 196 m_instructions.append({ PathInstructionType::QuadraticBezierCurve, absolute, parse_coordinate_pair_double() }); 197 if (match_comma_whitespace()) 198 parse_comma_whitespace(); 199 if (!match_coordinate()) 200 break; 201 } 202} 203 204void AttributeParser::parse_smooth_quadratic_bezier_curveto() 205{ 206 bool absolute = consume() == 'T'; 207 parse_whitespace(); 208 209 while (true) { 210 m_instructions.append({ PathInstructionType::SmoothQuadraticBezierCurve, absolute, parse_coordinate_pair() }); 211 if (match_comma_whitespace()) 212 parse_comma_whitespace(); 213 if (!match_coordinate()) 214 break; 215 } 216} 217 218void AttributeParser::parse_elliptical_arc() 219{ 220 bool absolute = consume() == 'A'; 221 parse_whitespace(); 222 223 while (true) { 224 m_instructions.append({ PathInstructionType::EllipticalArc, absolute, parse_elliptical_arg_argument() }); 225 if (match_comma_whitespace()) 226 parse_comma_whitespace(); 227 if (!match_coordinate()) 228 break; 229 } 230} 231 232float AttributeParser::parse_length() 233{ 234 // https://www.w3.org/TR/SVG11/types.html#DataTypeLength 235 return parse_number(); 236} 237 238float AttributeParser::parse_coordinate() 239{ 240 // https://www.w3.org/TR/SVG11/types.html#DataTypeCoordinate 241 // coordinate ::= length 242 return parse_length(); 243} 244 245Vector<float> AttributeParser::parse_coordinate_pair() 246{ 247 Vector<float> coordinates; 248 coordinates.append(parse_coordinate()); 249 if (match_comma_whitespace()) 250 parse_comma_whitespace(); 251 coordinates.append(parse_coordinate()); 252 return coordinates; 253} 254 255Vector<float> AttributeParser::parse_coordinate_sequence() 256{ 257 Vector<float> sequence; 258 while (true) { 259 sequence.append(parse_coordinate()); 260 if (match_comma_whitespace()) 261 parse_comma_whitespace(); 262 if (!match_comma_whitespace() && !match_coordinate()) 263 break; 264 } 265 return sequence; 266} 267 268Vector<Vector<float>> AttributeParser::parse_coordinate_pair_sequence() 269{ 270 Vector<Vector<float>> sequence; 271 while (true) { 272 sequence.append(parse_coordinate_pair()); 273 if (match_comma_whitespace()) 274 parse_comma_whitespace(); 275 if (!match_comma_whitespace() && !match_coordinate()) 276 break; 277 } 278 return sequence; 279} 280 281Vector<float> AttributeParser::parse_coordinate_pair_double() 282{ 283 Vector<float> coordinates; 284 coordinates.extend(parse_coordinate_pair()); 285 if (match_comma_whitespace()) 286 parse_comma_whitespace(); 287 coordinates.extend(parse_coordinate_pair()); 288 return coordinates; 289} 290 291Vector<float> AttributeParser::parse_coordinate_pair_triplet() 292{ 293 Vector<float> coordinates; 294 coordinates.extend(parse_coordinate_pair()); 295 if (match_comma_whitespace()) 296 parse_comma_whitespace(); 297 coordinates.extend(parse_coordinate_pair()); 298 if (match_comma_whitespace()) 299 parse_comma_whitespace(); 300 coordinates.extend(parse_coordinate_pair()); 301 return coordinates; 302} 303 304Vector<float> AttributeParser::parse_elliptical_arg_argument() 305{ 306 Vector<float> numbers; 307 numbers.append(parse_nonnegative_number()); 308 if (match_comma_whitespace()) 309 parse_comma_whitespace(); 310 numbers.append(parse_nonnegative_number()); 311 if (match_comma_whitespace()) 312 parse_comma_whitespace(); 313 numbers.append(parse_number()); 314 parse_comma_whitespace(); 315 numbers.append(parse_flag()); 316 if (match_comma_whitespace()) 317 parse_comma_whitespace(); 318 numbers.append(parse_flag()); 319 if (match_comma_whitespace()) 320 parse_comma_whitespace(); 321 numbers.extend(parse_coordinate_pair()); 322 323 return numbers; 324} 325 326void AttributeParser::parse_whitespace(bool must_match_once) 327{ 328 bool matched = false; 329 while (!done() && match_whitespace()) { 330 consume(); 331 matched = true; 332 } 333 334 VERIFY(!must_match_once || matched); 335} 336 337void AttributeParser::parse_comma_whitespace() 338{ 339 if (match(',')) { 340 consume(); 341 parse_whitespace(); 342 } else { 343 parse_whitespace(1); 344 if (match(',')) 345 consume(); 346 parse_whitespace(); 347 } 348} 349 350// https://www.w3.org/TR/SVG11/types.html#DataTypeNumber 351float AttributeParser::parse_number() 352{ 353 auto sign = parse_sign(); 354 return sign * parse_nonnegative_number(); 355} 356 357// https://www.w3.org/TR/SVG11/paths.html#PathDataBNF 358float AttributeParser::parse_nonnegative_number() 359{ 360 // NOTE: The grammar is almost a floating point except we cannot have a sign 361 // at the start. That condition should have been checked by the caller. 362 VERIFY(!match('+') && !match('-')); 363 364 auto remaining_source_text = m_source.substring_view(m_cursor); 365 char const* start = remaining_source_text.characters_without_null_termination(); 366 367 auto maybe_float = parse_first_floating_point<float>(start, start + remaining_source_text.length()); 368 VERIFY(maybe_float.parsed_value()); 369 m_cursor += maybe_float.end_ptr - start; 370 371 return maybe_float.value; 372} 373 374float AttributeParser::parse_flag() 375{ 376 if (!match('0') && !match('1')) 377 VERIFY_NOT_REACHED(); 378 return consume() - '0'; 379} 380 381int AttributeParser::parse_sign() 382{ 383 if (match('-')) { 384 consume(); 385 return -1; 386 } 387 if (match('+')) 388 consume(); 389 return 1; 390} 391 392bool AttributeParser::match_whitespace() const 393{ 394 if (done()) 395 return false; 396 char c = ch(); 397 return c == 0x9 || c == 0x20 || c == 0xa || c == 0xc || c == 0xd; 398} 399 400bool AttributeParser::match_comma_whitespace() const 401{ 402 return match_whitespace() || match(','); 403} 404 405bool AttributeParser::match_coordinate() const 406{ 407 return match_length(); 408} 409 410bool AttributeParser::match_length() const 411{ 412 return !done() && (isdigit(ch()) || ch() == '-' || ch() == '+' || ch() == '.'); 413} 414 415}