lex.odin (16447B)
1 package html 2 3 import "core:log" 4 import "core:math/rand" 5 import "core:os" 6 import "core:path/filepath" 7 import "core:strconv" 8 import "core:strings" 9 import "core:testing" 10 import "core:time" 11 import "core:unicode" 12 13 DOCTYPE_LITERAL :: "<!DOCTYPE" 14 15 // Token is a flat descriptor of a lexical token. 16 // 17 // `start` and `end` are inclusive byte offsets into the source string. 18 // For atoms (single-character tokens) `end == start`. 19 Token :: struct { 20 kind: Token_Kind, 21 start: u32, 22 end: u32, 23 } 24 25 Token_Kind :: enum u8 { 26 Open_Tag_Start, 27 Tag_End, 28 Close_Tag_Start, 29 Self_Closing_Tag_End, 30 Assign, 31 Comment_Start, 32 Comment_End, 33 Double_Quote_String_Literal, 34 Single_Quote_String_Literal, 35 Raw_Text, 36 Doctype, 37 } 38 39 Lexer :: struct { 40 cursor: u32, 41 source: string, 42 } 43 44 lexer_next :: proc(l: ^Lexer) -> (t: Token, ok: bool) { 45 if peek := lexer_peek(l, len(DOCTYPE_LITERAL)); peek == DOCTYPE_LITERAL { 46 start := l.cursor 47 if !lexer_advance_until(l, `>`) do return t, false 48 end := l.cursor 49 defer lexer_advance(l) 50 return Token{kind = .Doctype, start = start, end = end}, true 51 } 52 53 peek := lexer_peek(l) 54 if peek == "" { 55 return t, false 56 } 57 58 if t, ok := lex_token(l); ok { 59 return t, true 60 } 61 62 // Advance until we find an interesting token. 63 // The cursor is reset to undo the advancing that occurs within lex_token. 64 start := l.cursor 65 for { 66 if !lexer_advance(l) { 67 break 68 } 69 70 start := l.cursor 71 _, ok := lex_token(l) 72 l.cursor = start 73 74 if ok { 75 break 76 } 77 } 78 end := l.cursor - 1 79 80 return Token{kind = .Raw_Text, start = start, end = end}, true 81 } 82 83 @(private = "file") 84 lex_token :: proc(l: ^Lexer) -> (t: Token, ok: bool) { 85 if peek := lexer_peek(l, 4); peek == "<!--" { 86 defer lexer_advance(l, 4) 87 return Token{kind = .Comment_Start, start = l.cursor, end = l.cursor + 3}, true 88 } 89 90 if peek := lexer_peek(l, 3); peek == "-->" { 91 defer lexer_advance(l, 3) 92 return Token{kind = .Comment_End, start = l.cursor, end = l.cursor + 2}, true 93 } 94 95 if peek := lexer_peek(l, 2); peek != "" && peek[0] == '<' && unicode.is_letter(rune(peek[1])) { 96 lexer_advance(l) 97 start := l.cursor 98 if !lexer_advance_while_letters(l) do return 99 end := l.cursor 100 defer lexer_advance(l) 101 return Token{kind = .Open_Tag_Start, start = start, end = end}, true 102 } 103 104 if peek := lexer_peek(l, 2); peek == "</" { 105 lexer_advance(l, 2) 106 start := l.cursor 107 if !lexer_advance_while_letters(l) do return 108 end := l.cursor 109 defer lexer_advance(l) 110 return Token{kind = .Close_Tag_Start, start = start, end = end}, true 111 } 112 113 if peek := lexer_peek(l, 2); peek == "/>" { 114 defer lexer_advance(l, 2) 115 return Token{kind = .Self_Closing_Tag_End, start = l.cursor, end = l.cursor + 1}, true 116 } 117 118 if peek := lexer_peek(l, 1); peek == ">" { 119 defer lexer_advance(l, 1) 120 return Token{kind = .Tag_End, start = l.cursor, end = l.cursor}, true 121 } 122 123 if peek := lexer_peek(l, 1); peek == "=" { 124 defer lexer_advance(l, 1) 125 return Token{kind = .Assign, start = l.cursor, end = l.cursor}, true 126 } 127 128 if peek := lexer_peek(l, 1); peek == `"` { 129 start := l.cursor 130 lexer_advance(l, 1) 131 if !lexer_advance_until(l, `"`) do return t, false 132 end := l.cursor 133 defer lexer_advance(l, 1) 134 return Token{kind = .Double_Quote_String_Literal, start = start, end = end}, true 135 } 136 137 if peek := lexer_peek(l, 1); peek == `'` { 138 start := l.cursor 139 lexer_advance(l, 1) 140 if !lexer_advance_until(l, `'`) do return t, false 141 end := l.cursor 142 defer lexer_advance(l, 1) 143 return Token{kind = .Single_Quote_String_Literal, start = start, end = end}, true 144 } 145 146 return 147 148 } 149 150 lexer_expect :: proc(l: ^Lexer, kind: Token_Kind) -> (t: Token, ok: bool) { 151 t = lexer_next(l) or_return 152 return t, token_kind(t) == kind 153 } 154 155 lexer_advance :: proc(l: ^Lexer, step: u32 = 1) -> (ok: bool) { 156 l.cursor += step 157 return l.cursor < u32(len(l.source)) 158 } 159 160 // lexer_advance_until advances the cursor until the source at the cursor 161 // matches `target`. Returns true if a match was found, false on EOF. 162 lexer_advance_until :: proc(l: ^Lexer, target: string) -> bool { 163 for { 164 if lexer_peek(l, u32(len(target))) == target { 165 return true 166 } 167 if !lexer_advance(l) { 168 return false 169 } 170 } 171 } 172 173 // lexer_advance_while_letters advances the cursor while the byte at cursor+1 174 // is a letter, hyphen, or underscore. Returns true if stopped at a non-identifier 175 // byte, false on EOF. 176 lexer_advance_while_letters :: proc(l: ^Lexer) -> bool { 177 for { 178 b, ok := lexer_peek_byte(l, 1) 179 if !ok { 180 return false 181 } 182 if !(unicode.is_letter(rune(b)) || b == '-' || b == '_') { 183 return true 184 } 185 if !lexer_advance(l) { 186 return false 187 } 188 } 189 } 190 191 // lexer_advance_until_space advances the cursor until the byte at cursor+1 192 // is whitespace. Returns true if stopped, false on EOF. 193 lexer_advance_until_space :: proc(l: ^Lexer) -> bool { 194 for { 195 b, ok := lexer_peek_byte(l, 1) 196 if !ok { 197 return false 198 } 199 if unicode.is_space(rune(b)) { 200 return true 201 } 202 if !lexer_advance(l) { 203 return false 204 } 205 } 206 } 207 208 // lexer_advance_until_space_or advances the cursor until the byte at cursor+1 209 // is whitespace or equals `or`. Returns true if stopped, false on EOF. 210 lexer_advance_until_space_or :: proc(l: ^Lexer, or: byte) -> bool { 211 for { 212 b, ok := lexer_peek_byte(l, 1) 213 if !ok { 214 return false 215 } 216 if unicode.is_space(rune(b)) || b == or { 217 return true 218 } 219 if !lexer_advance(l) { 220 return false 221 } 222 } 223 } 224 225 lexer_peek :: proc(l: ^Lexer, step: u32 = 1) -> (p: string) { 226 start := l.cursor 227 end := l.cursor + step 228 229 if end > u32(len(l.source)) { 230 return "" 231 } 232 233 return l.source[start:end] 234 } 235 236 // lexer_peek_byte returns the byte at cursor+offset without advancing. 237 lexer_peek_byte :: proc(l: ^Lexer, offset: u32 = 0) -> (b: byte, ok: bool) { 238 pos := l.cursor + offset 239 if pos >= u32(len(l.source)) { 240 return 0, false 241 } 242 return l.source[pos], true 243 } 244 245 // lexer_peek_token returns the next token without advancing the lexer. 246 lexer_peek_token :: proc(l: ^Lexer) -> (t: Token, ok: bool) { 247 start := l.cursor 248 defer l.cursor = start 249 return lexer_next(l) 250 } 251 252 lexer_skip_space :: proc(l: ^Lexer) { 253 for { 254 b, ok := lexer_peek_byte(l) 255 if !ok { 256 return 257 } 258 if !unicode.is_space(rune(b)) { 259 return 260 } 261 if !lexer_advance(l) { 262 return 263 } 264 } 265 } 266 267 // token_kind returns the kind for the token. 268 token_kind :: proc(t: Token) -> Token_Kind { 269 return t.kind 270 } 271 272 // token_lookup returns the substring corresponding to the token. 273 // For atoms (end == start) this is the single byte at `start`; 274 // for spans it is the inclusive range [start, end]. 275 token_lookup :: proc(t: Token, text: string) -> string { 276 return text[t.start:t.end + 1] 277 } 278 279 @(private = "file") 280 check_tokens :: proc(t: ^testing.T, source: string, want: []Token, loc := #caller_location) { 281 defer free_all() 282 283 lexer: Lexer 284 lexer.source = source 285 286 got: [dynamic]Token 287 288 defer if testing.failed(t) { 289 log.debugf("got: %v", got) 290 } 291 292 for ii := 0; true; ii += 1 { 293 token, ok := lexer_next(&lexer) 294 append(&got, token) 295 296 log.debugf("%d/%d: token %v %v", ii + 1, len(want), ok, token, location = loc) 297 log.debugf("lexer: %v", lexer, location = loc) 298 299 if !ok { 300 if ii <= len(want) - 1 { 301 log.errorf( 302 "short output: got %d tokens; (%v), want %d %v", 303 ii, 304 token, 305 len(want), 306 want, 307 location = loc, 308 ) 309 } 310 break 311 } 312 313 if ii >= len(want) { 314 log.errorf("got more tokens than expected: %v", token, location = loc) 315 } else { 316 testing.expect_value(t, token, want[ii], loc = loc) 317 } 318 } 319 } 320 321 @(test) 322 test_lex_comment :: proc(t: ^testing.T) { 323 source := "<!-- comment -->" 324 want := []Token { 325 Token{kind = .Comment_Start, start = 0, end = 3}, 326 Token{kind = .Raw_Text, start = 4, end = 12}, 327 Token{kind = .Comment_End, start = 13, end = 15}, 328 } 329 check_tokens(t, source, want) 330 } 331 332 @(test) 333 test_lex_comment_2 :: proc(t: ^testing.T) { 334 source := "<!--comment-->" 335 want := []Token { 336 Token{kind = .Comment_Start, start = 0, end = 3}, 337 Token{kind = .Raw_Text, start = 4, end = 10}, 338 Token{kind = .Comment_End, start = 11, end = 13}, 339 } 340 check_tokens(t, source, want) 341 } 342 343 @(test) 344 test_lex_comment_3 :: proc(t: ^testing.T) { 345 source := "<!-- \ncomment\n -->" 346 want := []Token { 347 Token{kind = .Comment_Start, start = 0, end = 3}, 348 Token{kind = .Raw_Text, start = 4, end = 14}, 349 Token{kind = .Comment_End, start = 15, end = 17}, 350 } 351 check_tokens(t, source, want) 352 } 353 354 @(test) 355 test_lex_doctype :: proc(t: ^testing.T) { 356 source := "<!DOCTYPE html>" 357 want := []Token{Token{kind = .Doctype, start = 0, end = 14}} 358 check_tokens(t, source, want) 359 } 360 361 @(test) 362 test_lex_exotic_doctype :: proc(t: ^testing.T) { 363 source := "<!DOCTYPE foobar>" 364 want := []Token{Token{kind = .Doctype, start = 0, end = 16}} 365 check_tokens(t, source, want) 366 } 367 368 @(test) 369 test_lex_self_closing_tag :: proc(t: ^testing.T) { 370 source := `<tag />` 371 want := []Token { 372 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 373 Token{kind = .Raw_Text, start = 4, end = 4}, 374 Token{kind = .Self_Closing_Tag_End, start = 5, end = 6}, 375 } 376 check_tokens(t, source, want) 377 } 378 379 @(test) 380 test_lex_self_closing_tag_2 :: proc(t: ^testing.T) { 381 source := `<tag/>` 382 want := []Token { 383 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 384 Token{kind = .Self_Closing_Tag_End, start = 4, end = 5}, 385 } 386 check_tokens(t, source, want) 387 } 388 389 @(test) 390 test_lex_open_tag :: proc(t: ^testing.T) { 391 source := "<tag>" 392 want := []Token { 393 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 394 Token{kind = .Tag_End, start = 4, end = 4}, 395 } 396 check_tokens(t, source, want) 397 } 398 399 @(test) 400 test_lex_close_tag :: proc(t: ^testing.T) { 401 source := "</tag>" 402 want := []Token { 403 Token{kind = .Close_Tag_Start, start = 2, end = 4}, 404 Token{kind = .Tag_End, start = 5, end = 5}, 405 } 406 check_tokens(t, source, want) 407 } 408 409 @(test) 410 test_lex_tag_pair :: proc(t: ^testing.T) { 411 source := "<tag></tag>" 412 want := []Token { 413 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 414 Token{kind = .Tag_End, start = 4, end = 4}, 415 Token{kind = .Close_Tag_Start, start = 7, end = 9}, 416 Token{kind = .Tag_End, start = 10, end = 10}, 417 } 418 check_tokens(t, source, want) 419 } 420 421 @(test) 422 test_lex_tag_pair_with_comment :: proc(t: ^testing.T) { 423 source := "<tag><!--comment--></tag>" 424 want := []Token { 425 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 426 Token{kind = .Tag_End, start = 4, end = 4}, 427 Token{kind = .Comment_Start, start = 5, end = 8}, 428 Token{kind = .Raw_Text, start = 9, end = 15}, 429 Token{kind = .Comment_End, start = 16, end = 18}, 430 Token{kind = .Close_Tag_Start, start = 21, end = 23}, 431 Token{kind = .Tag_End, start = 24, end = 24}, 432 } 433 check_tokens(t, source, want) 434 } 435 436 @(test) 437 test_lex_open_tag_value_attribute :: proc(t: ^testing.T) { 438 source := `<tag class=".style">` 439 want := []Token { 440 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 441 Token{kind = .Raw_Text, start = 4, end = 9}, 442 Token{kind = .Assign, start = 10, end = 10}, 443 Token{kind = .Double_Quote_String_Literal, start = 11, end = 18}, 444 Token{kind = .Tag_End, start = 19, end = 19}, 445 } 446 check_tokens(t, source, want) 447 } 448 449 @(test) 450 test_lex_open_tag_naked_attribute :: proc(t: ^testing.T) { 451 source := `<tag class=".style" foobar>` 452 want := []Token { 453 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 454 Token{kind = .Raw_Text, start = 4, end = 9}, 455 Token{kind = .Assign, start = 10, end = 10}, 456 Token{kind = .Double_Quote_String_Literal, start = 11, end = 18}, 457 Token{kind = .Raw_Text, start = 19, end = 25}, 458 Token{kind = .Tag_End, start = 26, end = 26}, 459 } 460 check_tokens(t, source, want) 461 } 462 463 @(test) 464 test_lex_nested_tags :: proc(t: ^testing.T) { 465 source := `<tag><ul><li>one</li><li>two</li></ul></tag>` 466 want := []Token { 467 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 468 Token{kind = .Tag_End, start = 4, end = 4}, 469 Token{kind = .Open_Tag_Start, start = 6, end = 7}, 470 Token{kind = .Tag_End, start = 8, end = 8}, 471 Token{kind = .Open_Tag_Start, start = 10, end = 11}, 472 Token{kind = .Tag_End, start = 12, end = 12}, 473 Token{kind = .Raw_Text, start = 13, end = 15}, 474 Token{kind = .Close_Tag_Start, start = 18, end = 19}, 475 Token{kind = .Tag_End, start = 20, end = 20}, 476 Token{kind = .Open_Tag_Start, start = 22, end = 23}, 477 Token{kind = .Tag_End, start = 24, end = 24}, 478 Token{kind = .Raw_Text, start = 25, end = 27}, 479 Token{kind = .Close_Tag_Start, start = 30, end = 31}, 480 Token{kind = .Tag_End, start = 32, end = 32}, 481 Token{kind = .Close_Tag_Start, start = 35, end = 36}, 482 Token{kind = .Tag_End, start = 37, end = 37}, 483 Token{kind = .Close_Tag_Start, start = 40, end = 42}, 484 Token{kind = .Tag_End, start = 43, end = 43}, 485 } 486 check_tokens(t, source, want) 487 } 488 489 @(test) 490 test_lex_raw_text_less_than :: proc(t: ^testing.T) { 491 source := `<tag> 1 < 2 </tag>` 492 want := []Token { 493 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 494 Token{kind = .Tag_End, start = 4, end = 4}, 495 Token{kind = .Raw_Text, start = 5, end = 11}, 496 Token{kind = .Close_Tag_Start, start = 14, end = 16}, 497 Token{kind = .Tag_End, start = 17, end = 17}, 498 } 499 check_tokens(t, source, want) 500 } 501 502 @(test) 503 test_lex_invalid_tag :: proc(t: ^testing.T) { 504 source := `<tag> one <two three </tag>` 505 want := []Token { 506 Token{kind = .Open_Tag_Start, start = 1, end = 3}, 507 Token{kind = .Tag_End, start = 4, end = 4}, 508 Token{kind = .Raw_Text, start = 5, end = 9}, 509 Token{kind = .Open_Tag_Start, start = 11, end = 13}, 510 Token{kind = .Raw_Text, start = 14, end = 20}, 511 Token{kind = .Close_Tag_Start, start = 23, end = 25}, 512 Token{kind = .Tag_End, start = 26, end = 26}, 513 } 514 check_tokens(t, source, want) 515 } 516 517 FUZZ_ENABLED :: #config(FUZZ, false) 518 FUZZ_RUN_FAILED :: #config(FUZZ_RUN_FAILED, false) 519 520 @(test) 521 @(disabled = !FUZZ_ENABLED) 522 test_lex_fuzz_lexer :: proc(t: ^testing.T) { 523 if !FUZZ_ENABLED do return 524 525 target: string 526 527 // cleanup saves the failed fuzz target to a file. 528 testing.cleanup(t, proc(userdata: rawptr) { 529 target: ^string = auto_cast userdata 530 531 if target == nil || target^ == "" { 532 return 533 } 534 535 buf: [64]byte 536 537 name := strconv.write_int(buf[:], i64(time.time_to_unix(time.now())), 10) 538 539 if err := os.make_directory("fuzzdata"); err != nil && err != .Exist { 540 log.errorf("preparing fuzzdata directory: %w", err) 541 } 542 543 file_path, _ := filepath.join([]string{"fuzzdata", name}) 544 545 if err := os.write_entire_file_from_string(file_path, target^); err != nil { 546 log.errorf("writing to fuzz corpus file: %v", err) 547 } 548 549 }, &target) 550 551 log.info("fuzzing ...") 552 553 // Run the known failed fuzz corpus. 554 if FUZZ_RUN_FAILED { 555 dir, err := os.open("fuzzdata") 556 if err != nil { 557 log.errorf("opening fuzzdata directory: %v", err) 558 } 559 560 defer os.close(dir) 561 562 entries, entries_err := os.read_directory(dir, -1, context.allocator) 563 if entries_err != nil { 564 log.errorf("reading fuzzdata entries: %v", err) 565 } 566 567 defer { 568 for entry in entries { 569 os.file_info_delete(entry, context.allocator) 570 } 571 delete(entries) 572 } 573 574 for entry in entries { 575 input, input_err := os.read_entire_file_from_path(entry.fullpath, context.allocator) 576 if input_err != nil { 577 log.errorf("reading fuzzdata entry: %v", err) 578 continue 579 } 580 581 l: Lexer 582 l.source = string(input) 583 584 log.debugf("%q", l.source) 585 lexer_print_tokens(&l) 586 } 587 588 return 589 } 590 591 // Explore new randomized inputs. 592 593 rand.reset(t.seed) 594 595 // Define the pool characters. 596 // Whitespace and angle brackets are weighted higher. 597 character_set :: "abcdefghijklmnopqrstuvwxyz <<<<<>>>>>/=\"!" 598 599 for ii := 1; true; ii += 1 { 600 source := random_string(character_set, rand.int_max(100)) 601 defer strings.builder_destroy(&source) 602 603 src := strings.to_string(source) 604 target = src 605 606 l: Lexer 607 l.source = src 608 609 for { 610 _, ok := lexer_next(&l) 611 if !ok { 612 break 613 } 614 } 615 } 616 } 617 618 lexer_print_tokens :: proc(l: ^Lexer) { 619 for token in lexer_next(l) { 620 context.allocator = context.temp_allocator 621 defer free_all() 622 if token.start == token.end { 623 log.debug(token) 624 log.debugf("\t: %q \t(%v)", rune(l.source[token.start]), token) 625 log.debugf("\t: %v", l.source) 626 log.debugf("\t: %v^", strings.repeat(" ", int(token.start))) 627 } else { 628 log.debugf("\t: %q \t(%v)", l.source[token.start:token.end + 1], token) 629 log.debugf("\t: %v", l.source) 630 padding := strings.repeat(" ", int(token.start)) 631 underline := "" 632 if token.end - token.start - 1 >= 0 { 633 underline = strings.repeat("_", int(token.end - token.start - 1)) 634 } 635 log.debugf("\t: %v^%v^", padding, underline) 636 } 637 } 638 } 639 640 random_string :: proc(char_set: string, length: int) -> strings.Builder { 641 b: strings.Builder 642 643 for ii in 0 ..< length { 644 strings.write_byte(&b, char_set[rand.int_max(len(char_set))]) 645 } 646 647 return b 648 } 649