test_parseDeclarations.js (36029B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { 7 parseDeclarations, 8 _parseCommentDeclarations, 9 parseNamedDeclarations, 10 } = require("resource://devtools/shared/css/parsing-utils.js"); 11 const { 12 isCssPropertyKnown, 13 } = require("resource://devtools/server/actors/css-properties.js"); 14 15 const TEST_DATA = [ 16 // Simple test 17 { 18 input: "p:v;", 19 expected: [ 20 { 21 name: "p", 22 value: "v", 23 priority: "", 24 offsets: [0, 4], 25 declarationText: "p:v;", 26 }, 27 ], 28 }, 29 // Simple test 30 { 31 input: "this:is;a:test;", 32 expected: [ 33 { 34 name: "this", 35 value: "is", 36 priority: "", 37 offsets: [0, 8], 38 declarationText: "this:is;", 39 }, 40 { 41 name: "a", 42 value: "test", 43 priority: "", 44 offsets: [8, 15], 45 declarationText: "a:test;", 46 }, 47 ], 48 }, 49 // Test a single declaration with semi-colon 50 { 51 input: "name:value;", 52 expected: [ 53 { 54 name: "name", 55 value: "value", 56 priority: "", 57 offsets: [0, 11], 58 declarationText: "name:value;", 59 }, 60 ], 61 }, 62 // Test a single declaration without semi-colon 63 { 64 input: "name:value", 65 expected: [ 66 { 67 name: "name", 68 value: "value", 69 priority: "", 70 offsets: [0, 10], 71 declarationText: "name:value", 72 }, 73 ], 74 }, 75 // Test multiple declarations separated by whitespaces and carriage 76 // returns and tabs 77 { 78 input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;", 79 expected: [ 80 { 81 name: "p1", 82 value: "v1", 83 priority: "", 84 offsets: [0, 9], 85 declarationText: "p1 : v1 ;", 86 }, 87 { 88 name: "p2", 89 value: "v2", 90 priority: "", 91 offsets: [16, 22], 92 declarationText: "p2:v2;", 93 }, 94 { 95 name: "p3", 96 value: "v3", 97 priority: "", 98 offsets: [32, 45], 99 declarationText: "p3 : v3;", 100 }, 101 ], 102 }, 103 // Test simple priority 104 { 105 input: "p1: v1; p2: v2 !important;", 106 expected: [ 107 { 108 name: "p1", 109 value: "v1", 110 priority: "", 111 offsets: [0, 7], 112 declarationText: "p1: v1;", 113 }, 114 { 115 name: "p2", 116 value: "v2", 117 priority: "important", 118 offsets: [8, 26], 119 declarationText: "p2: v2 !important;", 120 }, 121 ], 122 }, 123 // Test simple priority 124 { 125 input: "p1: v1 !important; p2: v2", 126 expected: [ 127 { 128 name: "p1", 129 value: "v1", 130 priority: "important", 131 offsets: [0, 18], 132 declarationText: "p1: v1 !important;", 133 }, 134 { 135 name: "p2", 136 value: "v2", 137 priority: "", 138 offsets: [19, 25], 139 declarationText: "p2: v2", 140 }, 141 ], 142 }, 143 // Test simple priority 144 { 145 input: "p1: v1 ! important; p2: v2 ! important;", 146 expected: [ 147 { 148 name: "p1", 149 value: "v1", 150 priority: "important", 151 offsets: [0, 20], 152 declarationText: "p1: v1 ! important;", 153 }, 154 { 155 name: "p2", 156 value: "v2", 157 priority: "important", 158 offsets: [21, 40], 159 declarationText: "p2: v2 ! important;", 160 }, 161 ], 162 }, 163 // Test simple priority 164 { 165 input: "p1: v1 !/*comment*/important;", 166 expected: [ 167 { 168 name: "p1", 169 value: "v1", 170 priority: "important", 171 offsets: [0, 29], 172 declarationText: "p1: v1 !/*comment*/important;", 173 }, 174 ], 175 }, 176 // Test priority without terminating ";". 177 { 178 input: "p1: v1 !important", 179 expected: [ 180 { 181 name: "p1", 182 value: "v1", 183 priority: "important", 184 offsets: [0, 17], 185 declarationText: "p1: v1 !important", 186 }, 187 ], 188 }, 189 // Test trailing "!" without terminating ";". 190 { 191 input: "p1: v1 !", 192 expected: [ 193 { 194 name: "p1", 195 value: "v1 !", 196 priority: "", 197 offsets: [0, 8], 198 declarationText: "p1: v1 !", 199 }, 200 ], 201 }, 202 // Test invalid priority 203 { 204 input: "p1: v1 important;", 205 expected: [ 206 { 207 name: "p1", 208 value: "v1 important", 209 priority: "", 210 offsets: [0, 17], 211 declarationText: "p1: v1 important;", 212 }, 213 ], 214 }, 215 // Test invalid priority (in the middle of the declaration). 216 // See bug 1462553. 217 { 218 input: "p1: v1 !important v2;", 219 expected: [ 220 { 221 name: "p1", 222 value: "v1 !important v2", 223 priority: "", 224 offsets: [0, 21], 225 declarationText: "p1: v1 !important v2;", 226 }, 227 ], 228 }, 229 { 230 input: "p1: v1 ! important v2;", 231 expected: [ 232 { 233 name: "p1", 234 value: "v1 ! important v2", 235 priority: "", 236 offsets: [0, 25], 237 declarationText: "p1: v1 ! important v2;", 238 }, 239 ], 240 }, 241 { 242 input: "p1: v1 ! /*comment*/ important v2;", 243 expected: [ 244 { 245 name: "p1", 246 value: "v1 ! important v2", 247 priority: "", 248 offsets: [0, 36], 249 declarationText: "p1: v1 ! /*comment*/ important v2;", 250 }, 251 ], 252 }, 253 { 254 input: "p1: v1 !/*hi*/important v2;", 255 expected: [ 256 { 257 name: "p1", 258 value: "v1 ! important v2", 259 priority: "", 260 offsets: [0, 27], 261 declarationText: "p1: v1 !/*hi*/important v2;", 262 }, 263 ], 264 }, 265 // Test various types of background-image urls 266 { 267 input: "background-image: url(../../relative/image.png)", 268 expected: [ 269 { 270 name: "background-image", 271 value: "url(../../relative/image.png)", 272 priority: "", 273 offsets: [0, 47], 274 declarationText: "background-image: url(../../relative/image.png)", 275 }, 276 ], 277 }, 278 { 279 input: "background-image: url(http://site.com/test.png)", 280 expected: [ 281 { 282 name: "background-image", 283 value: "url(http://site.com/test.png)", 284 priority: "", 285 offsets: [0, 47], 286 declarationText: "background-image: url(http://site.com/test.png)", 287 }, 288 ], 289 }, 290 { 291 input: "background-image: url(wow.gif)", 292 expected: [ 293 { 294 name: "background-image", 295 value: "url(wow.gif)", 296 priority: "", 297 offsets: [0, 30], 298 declarationText: "background-image: url(wow.gif)", 299 }, 300 ], 301 }, 302 // Test that urls with :;{} characters in them are parsed correctly 303 { 304 input: 305 'background: red url("http://site.com/image{}:;.png?id=4#wat") ' + 306 "repeat top right", 307 expected: [ 308 { 309 name: "background", 310 value: 311 'red url("http://site.com/image{}:;.png?id=4#wat") ' + 312 "repeat top right", 313 priority: "", 314 offsets: [0, 78], 315 declarationText: 316 'background: red url("http://site.com/image{}:;.png?id=4#wat") ' + 317 "repeat top right", 318 }, 319 ], 320 }, 321 // Test that an empty string results in an empty array 322 { input: "", expected: [] }, 323 // Test that a string comprised only of whitespaces results in an empty array 324 { input: " \n \n \n \n \t \t\t\t ", expected: [] }, 325 // Test that a null input throws an exception 326 { input: null, throws: true }, 327 // Test that a undefined input throws an exception 328 { input: undefined, throws: true }, 329 // Test that :;{} characters in quoted content are not parsed as multiple 330 // declarations 331 { 332 input: 'content: ";color:red;}selector{color:yellow;"', 333 expected: [ 334 { 335 name: "content", 336 value: '";color:red;}selector{color:yellow;"', 337 priority: "", 338 offsets: [0, 45], 339 declarationText: 'content: ";color:red;}selector{color:yellow;"', 340 }, 341 ], 342 }, 343 // Test that rules aren't parsed, just declarations. 344 { 345 input: "body {color:red;} p {color: blue;}", 346 expected: [], 347 }, 348 // Test unbalanced : and ; 349 { 350 input: "color :red : font : arial;", 351 expected: [ 352 { 353 name: "color", 354 value: "red : font : arial", 355 priority: "", 356 offsets: [0, 26], 357 }, 358 ], 359 }, 360 { 361 input: "background: red;;;;;", 362 expected: [ 363 { 364 name: "background", 365 value: "red", 366 priority: "", 367 offsets: [0, 16], 368 declarationText: "background: red;", 369 }, 370 ], 371 }, 372 { 373 input: "background:;", 374 expected: [ 375 { name: "background", value: "", priority: "", offsets: [0, 12] }, 376 ], 377 }, 378 { input: ";;;;;", expected: [] }, 379 { input: ":;:;", expected: [] }, 380 // Test name only 381 { 382 input: "color", 383 expected: [{ name: "color", value: "", priority: "", offsets: [0, 5] }], 384 }, 385 // Test trailing name without : 386 { 387 input: "color:blue;font", 388 expected: [ 389 { 390 name: "color", 391 value: "blue", 392 priority: "", 393 offsets: [0, 11], 394 declarationText: "color:blue;", 395 }, 396 { name: "font", value: "", priority: "", offsets: [11, 15] }, 397 ], 398 }, 399 // Test trailing name with : 400 { 401 input: "color:blue;font:", 402 expected: [ 403 { 404 name: "color", 405 value: "blue", 406 priority: "", 407 offsets: [0, 11], 408 declarationText: "color:blue;", 409 }, 410 { name: "font", value: "", priority: "", offsets: [11, 16] }, 411 ], 412 }, 413 // Test leading value 414 { 415 input: "Arial;color:blue;", 416 expected: [ 417 { name: "", value: "Arial", priority: "", offsets: [0, 6] }, 418 { 419 name: "color", 420 value: "blue", 421 priority: "", 422 offsets: [6, 17], 423 declarationText: "color:blue;", 424 }, 425 ], 426 }, 427 // Test hex colors 428 { 429 input: "color: #333", 430 expected: [ 431 { 432 name: "color", 433 value: "#333", 434 priority: "", 435 offsets: [0, 11], 436 declarationText: "color: #333", 437 }, 438 ], 439 }, 440 { 441 input: "color: #456789", 442 expected: [ 443 { 444 name: "color", 445 value: "#456789", 446 priority: "", 447 offsets: [0, 14], 448 declarationText: "color: #456789", 449 }, 450 ], 451 }, 452 { 453 input: "wat: #XYZ", 454 expected: [ 455 { 456 name: "wat", 457 value: "#XYZ", 458 priority: "", 459 offsets: [0, 9], 460 declarationText: "wat: #XYZ", 461 }, 462 ], 463 }, 464 // Test string/url quotes escaping 465 { 466 input: "content: \"this is a 'string'\"", 467 expected: [ 468 { 469 name: "content", 470 value: "\"this is a 'string'\"", 471 priority: "", 472 offsets: [0, 29], 473 declarationText: "content: \"this is a 'string'\"", 474 }, 475 ], 476 }, 477 { 478 input: 'content: "this is a \\"string\\""', 479 expected: [ 480 { 481 name: "content", 482 value: '"this is a \\"string\\""', 483 priority: "", 484 offsets: [0, 31], 485 declarationText: 'content: "this is a \\"string\\""', 486 }, 487 ], 488 }, 489 { 490 input: "content: 'this is a \"string\"'", 491 expected: [ 492 { 493 name: "content", 494 value: "'this is a \"string\"'", 495 priority: "", 496 offsets: [0, 29], 497 declarationText: "content: 'this is a \"string\"'", 498 }, 499 ], 500 }, 501 { 502 input: "content: 'this is a \\'string\\''", 503 expected: [ 504 { 505 name: "content", 506 value: "'this is a \\'string\\''", 507 priority: "", 508 offsets: [0, 31], 509 declarationText: "content: 'this is a \\'string\\''", 510 }, 511 ], 512 }, 513 { 514 input: "content: 'this \\' is a \" really strange string'", 515 expected: [ 516 { 517 name: "content", 518 value: "'this \\' is a \" really strange string'", 519 priority: "", 520 offsets: [0, 47], 521 declarationText: "content: 'this \\' is a \" really strange string'", 522 }, 523 ], 524 }, 525 { 526 input: 'content: "a not s\\ o very long title"', 527 expected: [ 528 { 529 name: "content", 530 value: '"a not s\\ o very long title"', 531 priority: "", 532 offsets: [0, 46], 533 declarationText: 'content: "a not s\\ o very long title"', 534 }, 535 ], 536 }, 537 // Test calc with nested parentheses 538 { 539 input: "width: calc((100% - 3em) / 2)", 540 expected: [ 541 { 542 name: "width", 543 value: "calc((100% - 3em) / 2)", 544 priority: "", 545 offsets: [0, 29], 546 declarationText: "width: calc((100% - 3em) / 2)", 547 }, 548 ], 549 }, 550 551 // Simple embedded comment test. 552 { 553 parseComments: true, 554 input: "width: 5; /* background: green; */ background: red;", 555 expected: [ 556 { 557 name: "width", 558 value: "5", 559 priority: "", 560 offsets: [0, 9], 561 declarationText: "width: 5;", 562 }, 563 { 564 name: "background", 565 value: "green", 566 priority: "", 567 offsets: [13, 31], 568 declarationText: "background: green;", 569 commentOffsets: [10, 34], 570 }, 571 { 572 name: "background", 573 value: "red", 574 priority: "", 575 offsets: [35, 51], 576 declarationText: "background: red;", 577 }, 578 ], 579 }, 580 581 // Embedded comment where the parsing heuristic fails. 582 { 583 parseComments: true, 584 input: "width: 5; /* background something: green; */ background: red;", 585 expected: [ 586 { 587 name: "width", 588 value: "5", 589 priority: "", 590 offsets: [0, 9], 591 declarationText: "width: 5;", 592 }, 593 { 594 name: "background", 595 value: "red", 596 priority: "", 597 offsets: [45, 61], 598 declarationText: "background: red;", 599 }, 600 ], 601 }, 602 603 // Embedded comment where the parsing heuristic is a bit funny. 604 { 605 parseComments: true, 606 input: "width: 5; /* background: */ background: red;", 607 expected: [ 608 { 609 name: "width", 610 value: "5", 611 priority: "", 612 offsets: [0, 9], 613 declarationText: "width: 5;", 614 }, 615 { 616 name: "background", 617 value: "", 618 priority: "", 619 offsets: [13, 24], 620 commentOffsets: [10, 27], 621 }, 622 { 623 name: "background", 624 value: "red", 625 priority: "", 626 offsets: [28, 44], 627 declarationText: "background: red;", 628 }, 629 ], 630 }, 631 632 // Another case where the parsing heuristic says not to bother; note 633 // that there is no ";" in the comment. 634 { 635 parseComments: true, 636 input: "width: 5; /* background: yellow */ background: red;", 637 expected: [ 638 { 639 name: "width", 640 value: "5", 641 priority: "", 642 offsets: [0, 9], 643 declarationText: "width: 5;", 644 }, 645 { 646 name: "background", 647 value: "yellow", 648 priority: "", 649 offsets: [13, 31], 650 declarationText: "background: yellow", 651 commentOffsets: [10, 34], 652 }, 653 { 654 name: "background", 655 value: "red", 656 priority: "", 657 offsets: [35, 51], 658 declarationText: "background: red;", 659 }, 660 ], 661 }, 662 663 // Parsing a comment should yield text that has been unescaped, and 664 // the offsets should refer to the original text. 665 { 666 parseComments: true, 667 input: "/* content: '*\\/'; */", 668 expected: [ 669 { 670 name: "content", 671 value: "'*/'", 672 priority: "", 673 offsets: [3, 18], 674 declarationText: "content: '*\\/';", 675 commentOffsets: [0, 21], 676 }, 677 ], 678 }, 679 680 // Parsing a comment should yield text that has been unescaped, and 681 // the offsets should refer to the original text. This variant 682 // tests the no-semicolon path. 683 { 684 parseComments: true, 685 input: "/* content: '*\\/' */", 686 expected: [ 687 { 688 name: "content", 689 value: "'*/'", 690 priority: "", 691 offsets: [3, 17], 692 declarationText: "content: '*\\/'", 693 commentOffsets: [0, 20], 694 }, 695 ], 696 }, 697 698 // A comment-in-a-comment should yield the correct offsets. 699 { 700 parseComments: true, 701 input: "/* color: /\\* comment *\\/ red; */", 702 expected: [ 703 { 704 name: "color", 705 value: "red", 706 priority: "", 707 offsets: [3, 30], 708 declarationText: "color: /\\* comment *\\/ red;", 709 commentOffsets: [0, 33], 710 }, 711 ], 712 }, 713 714 // HTML comments are not special -- they are just ordinary tokens. 715 { 716 parseComments: true, 717 input: "<!-- color: red; --> color: blue;", 718 expected: [ 719 { name: "<!-- color", value: "red", priority: "", offsets: [0, 16] }, 720 { name: "--> color", value: "blue", priority: "", offsets: [17, 33] }, 721 ], 722 }, 723 724 // Don't error on an empty comment. 725 { 726 parseComments: true, 727 input: "/**/", 728 expected: [], 729 }, 730 731 // Parsing our special comments skips the name-check heuristic. 732 { 733 parseComments: true, 734 input: "/*! walrus: zebra; */", 735 expected: [ 736 { 737 name: "walrus", 738 value: "zebra", 739 priority: "", 740 offsets: [4, 18], 741 declarationText: "walrus: zebra;", 742 commentOffsets: [0, 21], 743 }, 744 ], 745 }, 746 747 // Regression test for bug 1287620. 748 { 749 input: "color: blue \\9 no\\_need", 750 expected: [ 751 { 752 name: "color", 753 value: "blue \\9 no\\_need", 754 priority: "", 755 offsets: [0, 23], 756 declarationText: "color: blue \\9 no\\_need", 757 }, 758 ], 759 }, 760 761 // Regression test for bug 1297890 - don't paste tokens. 762 { 763 parseComments: true, 764 input: "stroke-dasharray: 1/*ThisIsAComment*/2;", 765 expected: [ 766 { 767 name: "stroke-dasharray", 768 value: "1 2", 769 priority: "", 770 offsets: [0, 39], 771 declarationText: "stroke-dasharray: 1/*ThisIsAComment*/2;", 772 }, 773 ], 774 }, 775 776 // Regression test for bug 1384463 - don't trim significant 777 // whitespace. 778 { 779 // \u00a0 is non-breaking space. 780 input: "\u00a0vertical-align: top", 781 expected: [ 782 { 783 name: "\u00a0vertical-align", 784 value: "top", 785 priority: "", 786 offsets: [0, 20], 787 declarationText: "\u00a0vertical-align: top", 788 }, 789 ], 790 }, 791 792 // Regression test for bug 1544223 - take CSS blocks into consideration before handling 793 // ; and : (i.e. don't advance to the property name or value automatically). 794 { 795 input: `--foo: ( 796 :doodle { 797 @grid: 30x1 / 18vmin; 798 } 799 :container { 800 perspective: 30vmin; 801 } 802 803 @place-cell: center; 804 @size: 100%; 805 806 :after, :before { 807 content: ''; 808 @size: 100%; 809 position: absolute; 810 border: 2.4vmin solid var(--color); 811 border-image: radial-gradient( 812 var(--color), transparent 60% 813 ); 814 border-image-width: 4; 815 transform: rotate(@pn(0, 5deg)); 816 } 817 818 will-change: transform, opacity; 819 animation: scale-up 15s linear infinite; 820 animation-delay: calc(-15s / @size() * @i()); 821 box-shadow: inset 0 0 1em var(--color); 822 border-radius: 50%; 823 filter: var(--filter); 824 825 @keyframes scale-up { 826 0%, 100% { 827 transform: translateZ(0) scale(0) rotate(0); 828 opacity: 0; 829 } 830 50% { 831 opacity: 1; 832 } 833 99% { 834 transform: 835 translateZ(30vmin) 836 scale(1) 837 rotate(-270deg); 838 } 839 } 840 );`, 841 expected: [ 842 { 843 name: "--foo", 844 value: 845 "( :doodle { @grid: 30x1 / 18vmin; } :container { perspective: 30vmin; } " + 846 "@place-cell: center; @size: 100%; :after, :before { content: ''; @size: " + 847 "100%; position: absolute; border: 2.4vmin solid var(--color); " + 848 "border-image: radial-gradient( var(--color), transparent 60% ); " + 849 "border-image-width: 4; transform: rotate(@pn(0, 5deg)); } will-change: " + 850 "transform, opacity; animation: scale-up 15s linear infinite; " + 851 "animation-delay: calc(-15s / @size() * @i()); box-shadow: inset 0 0 1em " + 852 "var(--color); border-radius: 50%; filter: var(--filter); @keyframes " + 853 "scale-up { 0%, 100% { transform: translateZ(0) scale(0) rotate(0); " + 854 "opacity: 0; } 50% { opacity: 1; } 99% { transform: translateZ(30vmin) " + 855 "scale(1) rotate(-270deg); } } )", 856 priority: "", 857 offsets: [0, 1036], 858 isCustomProperty: true, 859 }, 860 ], 861 }, 862 863 /***** Testing nested rules *****/ 864 865 // Testing basic nesting with tagname selector 866 { 867 input: ` 868 color: red; 869 div { 870 background: blue; 871 } 872 `, 873 expected: [ 874 { 875 name: "color", 876 value: "red", 877 priority: "", 878 offsets: [7, 18], 879 declarationText: "color: red;", 880 }, 881 ], 882 }, 883 884 // Testing basic nesting with tagname + pseudo selector 885 { 886 input: ` 887 color: red; 888 div:hover { 889 background: blue; 890 } 891 `, 892 expected: [ 893 { 894 name: "color", 895 value: "red", 896 priority: "", 897 offsets: [7, 18], 898 declarationText: "color: red;", 899 }, 900 ], 901 }, 902 903 // Testing basic nesting with id selector 904 { 905 input: ` 906 color: red; 907 #myEl { 908 background: blue; 909 } 910 `, 911 expected: [ 912 { 913 name: "color", 914 value: "red", 915 priority: "", 916 offsets: [7, 18], 917 declarationText: "color: red;", 918 }, 919 ], 920 }, 921 922 // Testing basic nesting with class selector 923 { 924 input: ` 925 color: red; 926 .myEl { 927 background: blue; 928 } 929 `, 930 expected: [ 931 { 932 name: "color", 933 value: "red", 934 priority: "", 935 offsets: [7, 18], 936 declarationText: "color: red;", 937 }, 938 ], 939 }, 940 941 // Testing basic nesting with & + ident selector 942 { 943 input: ` 944 color: red; 945 & div { 946 background: blue; 947 } 948 `, 949 expected: [ 950 { 951 name: "color", 952 value: "red", 953 priority: "", 954 offsets: [7, 18], 955 declarationText: "color: red;", 956 }, 957 ], 958 }, 959 960 // Testing basic nesting with direct child selector 961 { 962 input: ` 963 color: red; 964 > div { 965 background: blue; 966 } 967 `, 968 expected: [ 969 { 970 name: "color", 971 value: "red", 972 priority: "", 973 offsets: [7, 18], 974 declarationText: "color: red;", 975 }, 976 ], 977 }, 978 979 // Testing basic nesting with & and :hover pseudo-class selector 980 { 981 input: ` 982 color: red; 983 &:hover { 984 background: blue; 985 } 986 `, 987 expected: [ 988 { 989 name: "color", 990 value: "red", 991 priority: "", 992 offsets: [7, 18], 993 declarationText: "color: red;", 994 }, 995 ], 996 }, 997 998 // Testing basic nesting with :not pseudo-class selector and non-leading & 999 { 1000 input: ` 1001 color: red; 1002 :not(&) { 1003 background: blue; 1004 } 1005 `, 1006 expected: [ 1007 { 1008 name: "color", 1009 value: "red", 1010 priority: "", 1011 offsets: [7, 18], 1012 declarationText: "color: red;", 1013 }, 1014 ], 1015 }, 1016 1017 // Testing basic nesting with attribute selector 1018 { 1019 input: ` 1020 color: red; 1021 [class] { 1022 background: blue; 1023 } 1024 `, 1025 expected: [ 1026 { 1027 name: "color", 1028 value: "red", 1029 priority: "", 1030 offsets: [7, 18], 1031 declarationText: "color: red;", 1032 }, 1033 ], 1034 }, 1035 1036 // Testing basic nesting with & compound selector 1037 { 1038 input: ` 1039 color: red; 1040 &div { 1041 background: blue; 1042 } 1043 `, 1044 expected: [ 1045 { 1046 name: "color", 1047 value: "red", 1048 priority: "", 1049 offsets: [7, 18], 1050 declarationText: "color: red;", 1051 }, 1052 ], 1053 }, 1054 1055 // Testing basic nesting with relative + selector 1056 { 1057 input: ` 1058 color: red; 1059 + div { 1060 background: blue; 1061 } 1062 `, 1063 expected: [ 1064 { 1065 name: "color", 1066 value: "red", 1067 priority: "", 1068 offsets: [7, 18], 1069 declarationText: "color: red;", 1070 }, 1071 ], 1072 }, 1073 1074 // Testing basic nesting with relative ~ selector 1075 { 1076 input: ` 1077 color: red; 1078 ~ div { 1079 background: blue; 1080 } 1081 `, 1082 expected: [ 1083 { 1084 name: "color", 1085 value: "red", 1086 priority: "", 1087 offsets: [7, 18], 1088 declarationText: "color: red;", 1089 }, 1090 ], 1091 }, 1092 1093 // Testing basic nesting with relative * selector 1094 { 1095 input: ` 1096 color: red; 1097 * div { 1098 background: blue; 1099 } 1100 `, 1101 expected: [ 1102 { 1103 name: "color", 1104 value: "red", 1105 priority: "", 1106 offsets: [7, 18], 1107 declarationText: "color: red;", 1108 }, 1109 ], 1110 }, 1111 1112 // Testing basic nesting after a property with !important 1113 { 1114 input: ` 1115 color: red !important; 1116 & div { 1117 background: blue; 1118 } 1119 `, 1120 expected: [ 1121 { 1122 name: "color", 1123 value: "red", 1124 priority: "important", 1125 offsets: [7, 29], 1126 declarationText: "color: red !important;", 1127 }, 1128 ], 1129 }, 1130 1131 // Testing basic nesting after a comment 1132 { 1133 input: ` 1134 color: red; 1135 /* nested rules */ 1136 & div { 1137 background: blue; 1138 } 1139 `, 1140 expected: [ 1141 { 1142 name: "color", 1143 value: "red", 1144 priority: "", 1145 offsets: [7, 18], 1146 declarationText: "color: red;", 1147 }, 1148 ], 1149 }, 1150 1151 // Testing at-rules (with condition) nesting 1152 { 1153 input: ` 1154 color: red; 1155 @media (orientation: landscape) { 1156 background: blue; 1157 } 1158 `, 1159 expected: [ 1160 { 1161 name: "color", 1162 value: "red", 1163 priority: "", 1164 offsets: [7, 18], 1165 declarationText: "color: red;", 1166 }, 1167 ], 1168 }, 1169 1170 // Testing at-rules (without condition) nesting 1171 { 1172 input: ` 1173 color: red; 1174 @media screen { 1175 background: blue; 1176 } 1177 `, 1178 expected: [ 1179 { 1180 name: "color", 1181 value: "red", 1182 priority: "", 1183 offsets: [7, 18], 1184 declarationText: "color: red;", 1185 }, 1186 ], 1187 }, 1188 1189 // Testing multi-level nesting 1190 { 1191 input: ` 1192 color: red; 1193 &div { 1194 &.active { 1195 border: 1px; 1196 } 1197 padding: 10px; 1198 } 1199 background: gold; 1200 `, 1201 expected: [ 1202 { 1203 name: "color", 1204 value: "red", 1205 priority: "", 1206 offsets: [7, 18], 1207 declarationText: "color: red;", 1208 }, 1209 ], 1210 }, 1211 1212 // Testing multi-level nesting with at-rules 1213 { 1214 input: ` 1215 color: red; 1216 @layer { 1217 background: yellow; 1218 @media screen { 1219 & { 1220 border-color: blue; 1221 } 1222 } 1223 } 1224 `, 1225 expected: [ 1226 { 1227 name: "color", 1228 value: "red", 1229 priority: "", 1230 offsets: [7, 18], 1231 declarationText: "color: red;", 1232 }, 1233 ], 1234 }, 1235 1236 // Testing sibling nested rules 1237 { 1238 input: ` 1239 color: red; 1240 .active { 1241 border: 1px; 1242 } 1243 &div { 1244 padding: 10px; 1245 } 1246 border-color: cyan 1247 `, 1248 expected: [ 1249 { 1250 name: "color", 1251 value: "red", 1252 priority: "", 1253 offsets: [7, 18], 1254 declarationText: "color: red;", 1255 }, 1256 ], 1257 }, 1258 1259 // Testing nesting interwined between property declarations 1260 { 1261 input: ` 1262 color: red; 1263 .active { 1264 border: 1px; 1265 } 1266 background: gold; 1267 &div { 1268 padding: 10px; 1269 } 1270 border-color: cyan 1271 `, 1272 expected: [ 1273 { 1274 name: "color", 1275 value: "red", 1276 priority: "", 1277 offsets: [7, 18], 1278 declarationText: "color: red;", 1279 }, 1280 ], 1281 }, 1282 1283 // Testing that "}" in content property does not impact the nested state 1284 { 1285 input: ` 1286 color: red; 1287 &div { 1288 content: "}" 1289 color: blue; 1290 } 1291 background: gold; 1292 `, 1293 expected: [ 1294 { 1295 name: "color", 1296 value: "red", 1297 priority: "", 1298 offsets: [7, 18], 1299 declarationText: "color: red;", 1300 }, 1301 ], 1302 }, 1303 1304 // Testing that "}" in attribute selector does not impact the nested state 1305 { 1306 input: ` 1307 color: red; 1308 + .foo { 1309 [class="}"] { 1310 padding: 10px; 1311 } 1312 } 1313 background: gold; 1314 `, 1315 expected: [ 1316 { 1317 name: "color", 1318 value: "red", 1319 priority: "", 1320 offsets: [7, 18], 1321 declarationText: "color: red;", 1322 }, 1323 ], 1324 }, 1325 1326 // Testing that in function does not impact the nested state 1327 { 1328 input: ` 1329 color: red; 1330 + .foo { 1331 background: url("img.png?x=}") 1332 } 1333 background: gold; 1334 `, 1335 expected: [ 1336 { 1337 name: "color", 1338 value: "red", 1339 priority: "", 1340 offsets: [7, 18], 1341 declarationText: "color: red;", 1342 }, 1343 ], 1344 }, 1345 1346 // Testing that "}" in comment does not impact the nested state 1347 { 1348 input: ` 1349 color: red; 1350 + .foo { 1351 /* Check } */ 1352 padding: 10px; 1353 } 1354 background: gold; 1355 `, 1356 expected: [ 1357 { 1358 name: "color", 1359 value: "red", 1360 priority: "", 1361 offsets: [7, 18], 1362 declarationText: "color: red;", 1363 }, 1364 ], 1365 }, 1366 1367 // Testing that nested rules in comments aren't reported 1368 { 1369 parseComments: true, 1370 input: "width: 5; /* div { color: cyan; } */ background: red;", 1371 expected: [ 1372 { 1373 name: "width", 1374 value: "5", 1375 priority: "", 1376 offsets: [0, 9], 1377 declarationText: "width: 5;", 1378 }, 1379 { 1380 name: "background", 1381 value: "red", 1382 priority: "", 1383 offsets: [37, 53], 1384 declarationText: "background: red;", 1385 }, 1386 ], 1387 }, 1388 1389 // Testing that declarations in comments are still handled while nested rule in same comment is ignored 1390 { 1391 parseComments: true, 1392 input: 1393 "width: 5; /* padding: 12px; div { color: cyan; } margin: 1em; */ background: red;", 1394 expected: [ 1395 { 1396 name: "width", 1397 value: "5", 1398 priority: "", 1399 offsets: [0, 9], 1400 declarationText: "width: 5;", 1401 }, 1402 { 1403 name: "padding", 1404 value: "12px", 1405 priority: "", 1406 offsets: [13, 27], 1407 declarationText: "padding: 12px;", 1408 }, 1409 { 1410 name: "margin", 1411 value: "1em", 1412 priority: "", 1413 offsets: [49, 61], 1414 declarationText: "margin: 1em;", 1415 }, 1416 { 1417 name: "background", 1418 value: "red", 1419 priority: "", 1420 offsets: [65, 81], 1421 declarationText: "background: red;", 1422 }, 1423 ], 1424 }, 1425 1426 // Testing nesting without closing bracket 1427 { 1428 input: ` 1429 color: red; 1430 & div { 1431 background: blue; 1432 `, 1433 expected: [ 1434 { 1435 name: "color", 1436 value: "red", 1437 priority: "", 1438 offsets: [7, 18], 1439 declarationText: "color: red;", 1440 }, 1441 ], 1442 }, 1443 1444 /***** Testing Custom Properties rules *****/ 1445 { 1446 input: ` 1447 --foo: 1; 1448 --bar: 2; 1449 --foobar: calc( var(--foo, 3) * var(--bar, 4)); 1450 --empty: ; 1451 width: var(--foobar, var(--fallback)); 1452 -moz-appearance: none; 1453 --: 5; 1454 `, 1455 expected: [ 1456 { 1457 name: "--foo", 1458 value: "1", 1459 priority: "", 1460 offsets: [7, 16], 1461 declarationText: "--foo: 1;", 1462 isCustomProperty: true, 1463 }, 1464 { 1465 name: "--bar", 1466 value: "2", 1467 priority: "", 1468 offsets: [23, 32], 1469 declarationText: "--bar: 2;", 1470 isCustomProperty: true, 1471 }, 1472 { 1473 name: "--foobar", 1474 value: "calc( var(--foo, 3) * var(--bar, 4))", 1475 priority: "", 1476 offsets: [39, 86], 1477 declarationText: "--foobar: calc( var(--foo, 3) * var(--bar, 4));", 1478 isCustomProperty: true, 1479 }, 1480 { 1481 name: "--empty", 1482 value: "", 1483 priority: "", 1484 offsets: [93, 103], 1485 declarationText: "--empty: ;", 1486 isCustomProperty: true, 1487 }, 1488 { 1489 name: "width", 1490 value: "var(--foobar, var(--fallback))", 1491 priority: "", 1492 offsets: [110, 148], 1493 declarationText: "width: var(--foobar, var(--fallback));", 1494 }, 1495 { 1496 name: "-moz-appearance", 1497 value: "none", 1498 priority: "", 1499 offsets: [155, 177], 1500 declarationText: "-moz-appearance: none;", 1501 }, 1502 { 1503 name: "--", 1504 value: "5", 1505 priority: "", 1506 offsets: [184, 190], 1507 declarationText: "--: 5;", 1508 }, 1509 ], 1510 }, 1511 ]; 1512 1513 function run_test() { 1514 run_basic_tests(); 1515 run_comment_tests(); 1516 run_named_tests(); 1517 } 1518 1519 // Test parseDeclarations. 1520 function run_basic_tests() { 1521 for (const test of TEST_DATA) { 1522 info("Test input string " + test.input); 1523 let output; 1524 try { 1525 output = parseDeclarations( 1526 isCssPropertyKnown, 1527 test.input, 1528 test.parseComments 1529 ); 1530 } catch (e) { 1531 info( 1532 "parseDeclarations threw an exception with the given input " + "string" 1533 ); 1534 if (test.throws) { 1535 info("Exception expected"); 1536 Assert.ok(true); 1537 } else { 1538 info("Exception unexpected\n" + e); 1539 Assert.ok(false); 1540 } 1541 } 1542 if (output) { 1543 assertDeclarations(test.input, output, test.expected); 1544 } 1545 } 1546 } 1547 1548 const COMMENT_DATA = [ 1549 { 1550 input: "content: 'hi", 1551 expected: [ 1552 { 1553 name: "content", 1554 value: "'hi", 1555 priority: "", 1556 terminator: "';", 1557 offsets: [2, 14], 1558 colonOffsets: [9, 11], 1559 commentOffsets: [0, 16], 1560 }, 1561 ], 1562 }, 1563 { 1564 input: "text that once confounded the parser;", 1565 expected: [], 1566 }, 1567 ]; 1568 1569 // Test parseCommentDeclarations. 1570 function run_comment_tests() { 1571 for (const test of COMMENT_DATA) { 1572 info("Test input string " + test.input); 1573 const output = _parseCommentDeclarations( 1574 isCssPropertyKnown, 1575 test.input, 1576 0, 1577 test.input.length + 4 1578 ); 1579 deepEqual(output, test.expected); 1580 } 1581 } 1582 1583 const NAMED_DATA = [ 1584 { 1585 input: "position:absolute;top50px;height:50px;", 1586 expected: [ 1587 { 1588 name: "position", 1589 value: "absolute", 1590 priority: "", 1591 terminator: "", 1592 offsets: [0, 18], 1593 colonOffsets: [8, 9], 1594 }, 1595 { 1596 name: "height", 1597 value: "50px", 1598 priority: "", 1599 terminator: "", 1600 offsets: [26, 38], 1601 colonOffsets: [32, 33], 1602 }, 1603 ], 1604 }, 1605 ]; 1606 1607 // Test parseNamedDeclarations. 1608 function run_named_tests() { 1609 for (const test of NAMED_DATA) { 1610 info("Test input string " + test.input); 1611 const output = parseNamedDeclarations(isCssPropertyKnown, test.input, true); 1612 info(JSON.stringify(output)); 1613 deepEqual(output, test.expected); 1614 } 1615 } 1616 1617 function assertDeclarations(input, actualDeclarations, expectedDeclarations) { 1618 if (actualDeclarations.length === expectedDeclarations.length) { 1619 for (let i = 0; i < expectedDeclarations.length; i++) { 1620 const actual = actualDeclarations[i]; 1621 const expected = expectedDeclarations[i]; 1622 Assert.ok(!!actual); 1623 info( 1624 "Check that the output item has the expected name, " + 1625 "value and priority" 1626 ); 1627 Assert.equal(actual.name, expected.name, "Expected name"); 1628 Assert.equal(actual.value, expected.value, "Expected value"); 1629 Assert.equal(actual.priority, expected.priority, "Expected priority"); 1630 deepEqual(actual.offsets, expected.offsets, "Expected offsets"); 1631 if ("commentOffsets" in expected) { 1632 deepEqual( 1633 actual.commentOffsets, 1634 expected.commentOffsets, 1635 "Expected commentOffsets" 1636 ); 1637 } 1638 1639 if (expected.declarationText) { 1640 Assert.equal( 1641 input.substring(expected.offsets[0], expected.offsets[1]), 1642 expected.declarationText, 1643 "Expected declarationText" 1644 ); 1645 } 1646 Assert.equal( 1647 actual.isCustomProperty, 1648 expected.isCustomProperty, 1649 "Expected isCustomProperty" 1650 ); 1651 } 1652 } else { 1653 for (const prop of actualDeclarations) { 1654 info( 1655 "Actual output contained: {name: " + 1656 prop.name + 1657 ", value: " + 1658 prop.value + 1659 ", priority: " + 1660 prop.priority + 1661 "}" 1662 ); 1663 } 1664 Assert.equal(actualDeclarations.length, expectedDeclarations.length); 1665 } 1666 }