1594 lines
33 KiB
JavaScript
1594 lines
33 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const {
|
|
parseDeclarations,
|
|
_parseCommentDeclarations,
|
|
parseNamedDeclarations,
|
|
} = require("resource://devtools/shared/css/parsing-utils.js");
|
|
const {
|
|
isCssPropertyKnown,
|
|
} = require("resource://devtools/server/actors/css-properties.js");
|
|
|
|
const TEST_DATA = [
|
|
// Simple test
|
|
{
|
|
input: "p:v;",
|
|
expected: [
|
|
{
|
|
name: "p",
|
|
value: "v",
|
|
priority: "",
|
|
offsets: [0, 4],
|
|
declarationText: "p:v;",
|
|
},
|
|
],
|
|
},
|
|
// Simple test
|
|
{
|
|
input: "this:is;a:test;",
|
|
expected: [
|
|
{
|
|
name: "this",
|
|
value: "is",
|
|
priority: "",
|
|
offsets: [0, 8],
|
|
declarationText: "this:is;",
|
|
},
|
|
{
|
|
name: "a",
|
|
value: "test",
|
|
priority: "",
|
|
offsets: [8, 15],
|
|
declarationText: "a:test;",
|
|
},
|
|
],
|
|
},
|
|
// Test a single declaration with semi-colon
|
|
{
|
|
input: "name:value;",
|
|
expected: [
|
|
{
|
|
name: "name",
|
|
value: "value",
|
|
priority: "",
|
|
offsets: [0, 11],
|
|
declarationText: "name:value;",
|
|
},
|
|
],
|
|
},
|
|
// Test a single declaration without semi-colon
|
|
{
|
|
input: "name:value",
|
|
expected: [
|
|
{
|
|
name: "name",
|
|
value: "value",
|
|
priority: "",
|
|
offsets: [0, 10],
|
|
declarationText: "name:value",
|
|
},
|
|
],
|
|
},
|
|
// Test multiple declarations separated by whitespaces and carriage
|
|
// returns and tabs
|
|
{
|
|
input: "p1 : v1 ; \t\t \n p2:v2; \n\n\n\n\t p3 : v3;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "p1 : v1 ;",
|
|
},
|
|
{
|
|
name: "p2",
|
|
value: "v2",
|
|
priority: "",
|
|
offsets: [16, 22],
|
|
declarationText: "p2:v2;",
|
|
},
|
|
{
|
|
name: "p3",
|
|
value: "v3",
|
|
priority: "",
|
|
offsets: [32, 45],
|
|
declarationText: "p3 : v3;",
|
|
},
|
|
],
|
|
},
|
|
// Test simple priority
|
|
{
|
|
input: "p1: v1; p2: v2 !important;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1",
|
|
priority: "",
|
|
offsets: [0, 7],
|
|
declarationText: "p1: v1;",
|
|
},
|
|
{
|
|
name: "p2",
|
|
value: "v2",
|
|
priority: "important",
|
|
offsets: [8, 26],
|
|
declarationText: "p2: v2 !important;",
|
|
},
|
|
],
|
|
},
|
|
// Test simple priority
|
|
{
|
|
input: "p1: v1 !important; p2: v2",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1",
|
|
priority: "important",
|
|
offsets: [0, 18],
|
|
declarationText: "p1: v1 !important;",
|
|
},
|
|
{
|
|
name: "p2",
|
|
value: "v2",
|
|
priority: "",
|
|
offsets: [19, 25],
|
|
declarationText: "p2: v2",
|
|
},
|
|
],
|
|
},
|
|
// Test simple priority
|
|
{
|
|
input: "p1: v1 ! important; p2: v2 ! important;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1",
|
|
priority: "important",
|
|
offsets: [0, 20],
|
|
declarationText: "p1: v1 ! important;",
|
|
},
|
|
{
|
|
name: "p2",
|
|
value: "v2",
|
|
priority: "important",
|
|
offsets: [21, 40],
|
|
declarationText: "p2: v2 ! important;",
|
|
},
|
|
],
|
|
},
|
|
// Test simple priority
|
|
{
|
|
input: "p1: v1 !/*comment*/important;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1",
|
|
priority: "important",
|
|
offsets: [0, 29],
|
|
declarationText: "p1: v1 !/*comment*/important;",
|
|
},
|
|
],
|
|
},
|
|
// Test priority without terminating ";".
|
|
{
|
|
input: "p1: v1 !important",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1",
|
|
priority: "important",
|
|
offsets: [0, 17],
|
|
declarationText: "p1: v1 !important",
|
|
},
|
|
],
|
|
},
|
|
// Test trailing "!" without terminating ";".
|
|
{
|
|
input: "p1: v1 !",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1 !",
|
|
priority: "",
|
|
offsets: [0, 8],
|
|
declarationText: "p1: v1 !",
|
|
},
|
|
],
|
|
},
|
|
// Test invalid priority
|
|
{
|
|
input: "p1: v1 important;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1 important",
|
|
priority: "",
|
|
offsets: [0, 17],
|
|
declarationText: "p1: v1 important;",
|
|
},
|
|
],
|
|
},
|
|
// Test invalid priority (in the middle of the declaration).
|
|
// See bug 1462553.
|
|
{
|
|
input: "p1: v1 !important v2;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1 !important v2",
|
|
priority: "",
|
|
offsets: [0, 21],
|
|
declarationText: "p1: v1 !important v2;",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "p1: v1 ! important v2;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1 ! important v2",
|
|
priority: "",
|
|
offsets: [0, 25],
|
|
declarationText: "p1: v1 ! important v2;",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "p1: v1 ! /*comment*/ important v2;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1 ! important v2",
|
|
priority: "",
|
|
offsets: [0, 36],
|
|
declarationText: "p1: v1 ! /*comment*/ important v2;",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "p1: v1 !/*hi*/important v2;",
|
|
expected: [
|
|
{
|
|
name: "p1",
|
|
value: "v1 ! important v2",
|
|
priority: "",
|
|
offsets: [0, 27],
|
|
declarationText: "p1: v1 !/*hi*/important v2;",
|
|
},
|
|
],
|
|
},
|
|
// Test various types of background-image urls
|
|
{
|
|
input: "background-image: url(../../relative/image.png)",
|
|
expected: [
|
|
{
|
|
name: "background-image",
|
|
value: "url(../../relative/image.png)",
|
|
priority: "",
|
|
offsets: [0, 47],
|
|
declarationText: "background-image: url(../../relative/image.png)",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "background-image: url(http://site.com/test.png)",
|
|
expected: [
|
|
{
|
|
name: "background-image",
|
|
value: "url(http://site.com/test.png)",
|
|
priority: "",
|
|
offsets: [0, 47],
|
|
declarationText: "background-image: url(http://site.com/test.png)",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "background-image: url(wow.gif)",
|
|
expected: [
|
|
{
|
|
name: "background-image",
|
|
value: "url(wow.gif)",
|
|
priority: "",
|
|
offsets: [0, 30],
|
|
declarationText: "background-image: url(wow.gif)",
|
|
},
|
|
],
|
|
},
|
|
// Test that urls with :;{} characters in them are parsed correctly
|
|
{
|
|
input:
|
|
'background: red url("http://site.com/image{}:;.png?id=4#wat") ' +
|
|
"repeat top right",
|
|
expected: [
|
|
{
|
|
name: "background",
|
|
value:
|
|
'red url("http://site.com/image{}:;.png?id=4#wat") ' +
|
|
"repeat top right",
|
|
priority: "",
|
|
offsets: [0, 78],
|
|
declarationText:
|
|
'background: red url("http://site.com/image{}:;.png?id=4#wat") ' +
|
|
"repeat top right",
|
|
},
|
|
],
|
|
},
|
|
// Test that an empty string results in an empty array
|
|
{ input: "", expected: [] },
|
|
// Test that a string comprised only of whitespaces results in an empty array
|
|
{ input: " \n \n \n \n \t \t\t\t ", expected: [] },
|
|
// Test that a null input throws an exception
|
|
{ input: null, throws: true },
|
|
// Test that a undefined input throws an exception
|
|
{ input: undefined, throws: true },
|
|
// Test that :;{} characters in quoted content are not parsed as multiple
|
|
// declarations
|
|
{
|
|
input: 'content: ";color:red;}selector{color:yellow;"',
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: '";color:red;}selector{color:yellow;"',
|
|
priority: "",
|
|
offsets: [0, 45],
|
|
declarationText: 'content: ";color:red;}selector{color:yellow;"',
|
|
},
|
|
],
|
|
},
|
|
// Test that rules aren't parsed, just declarations.
|
|
{
|
|
input: "body {color:red;} p {color: blue;}",
|
|
expected: [],
|
|
},
|
|
// Test unbalanced : and ;
|
|
{
|
|
input: "color :red : font : arial;",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red : font : arial",
|
|
priority: "",
|
|
offsets: [0, 26],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "background: red;;;;;",
|
|
expected: [
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [0, 16],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "background:;",
|
|
expected: [
|
|
{ name: "background", value: "", priority: "", offsets: [0, 12] },
|
|
],
|
|
},
|
|
{ input: ";;;;;", expected: [] },
|
|
{ input: ":;:;", expected: [] },
|
|
// Test name only
|
|
{
|
|
input: "color",
|
|
expected: [{ name: "color", value: "", priority: "", offsets: [0, 5] }],
|
|
},
|
|
// Test trailing name without :
|
|
{
|
|
input: "color:blue;font",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "blue",
|
|
priority: "",
|
|
offsets: [0, 11],
|
|
declarationText: "color:blue;",
|
|
},
|
|
{ name: "font", value: "", priority: "", offsets: [11, 15] },
|
|
],
|
|
},
|
|
// Test trailing name with :
|
|
{
|
|
input: "color:blue;font:",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "blue",
|
|
priority: "",
|
|
offsets: [0, 11],
|
|
declarationText: "color:blue;",
|
|
},
|
|
{ name: "font", value: "", priority: "", offsets: [11, 16] },
|
|
],
|
|
},
|
|
// Test leading value
|
|
{
|
|
input: "Arial;color:blue;",
|
|
expected: [
|
|
{ name: "", value: "Arial", priority: "", offsets: [0, 6] },
|
|
{
|
|
name: "color",
|
|
value: "blue",
|
|
priority: "",
|
|
offsets: [6, 17],
|
|
declarationText: "color:blue;",
|
|
},
|
|
],
|
|
},
|
|
// Test hex colors
|
|
{
|
|
input: "color: #333",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "#333",
|
|
priority: "",
|
|
offsets: [0, 11],
|
|
declarationText: "color: #333",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "color: #456789",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "#456789",
|
|
priority: "",
|
|
offsets: [0, 14],
|
|
declarationText: "color: #456789",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "wat: #XYZ",
|
|
expected: [
|
|
{
|
|
name: "wat",
|
|
value: "#XYZ",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "wat: #XYZ",
|
|
},
|
|
],
|
|
},
|
|
// Test string/url quotes escaping
|
|
{
|
|
input: "content: \"this is a 'string'\"",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "\"this is a 'string'\"",
|
|
priority: "",
|
|
offsets: [0, 29],
|
|
declarationText: "content: \"this is a 'string'\"",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: 'content: "this is a \\"string\\""',
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: '"this is a \\"string\\""',
|
|
priority: "",
|
|
offsets: [0, 31],
|
|
declarationText: 'content: "this is a \\"string\\""',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "content: 'this is a \"string\"'",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "'this is a \"string\"'",
|
|
priority: "",
|
|
offsets: [0, 29],
|
|
declarationText: "content: 'this is a \"string\"'",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "content: 'this is a \\'string\\''",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "'this is a \\'string\\''",
|
|
priority: "",
|
|
offsets: [0, 31],
|
|
declarationText: "content: 'this is a \\'string\\''",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "content: 'this \\' is a \" really strange string'",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "'this \\' is a \" really strange string'",
|
|
priority: "",
|
|
offsets: [0, 47],
|
|
declarationText: "content: 'this \\' is a \" really strange string'",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: 'content: "a not s\\ o very long title"',
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: '"a not s\\ o very long title"',
|
|
priority: "",
|
|
offsets: [0, 46],
|
|
declarationText: 'content: "a not s\\ o very long title"',
|
|
},
|
|
],
|
|
},
|
|
// Test calc with nested parentheses
|
|
{
|
|
input: "width: calc((100% - 3em) / 2)",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "calc((100% - 3em) / 2)",
|
|
priority: "",
|
|
offsets: [0, 29],
|
|
declarationText: "width: calc((100% - 3em) / 2)",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Simple embedded comment test.
|
|
{
|
|
parseComments: true,
|
|
input: "width: 5; /* background: green; */ background: red;",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "5",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "width: 5;",
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "green",
|
|
priority: "",
|
|
offsets: [13, 31],
|
|
declarationText: "background: green;",
|
|
commentOffsets: [10, 34],
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [35, 51],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Embedded comment where the parsing heuristic fails.
|
|
{
|
|
parseComments: true,
|
|
input: "width: 5; /* background something: green; */ background: red;",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "5",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "width: 5;",
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [45, 61],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Embedded comment where the parsing heuristic is a bit funny.
|
|
{
|
|
parseComments: true,
|
|
input: "width: 5; /* background: */ background: red;",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "5",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "width: 5;",
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "",
|
|
priority: "",
|
|
offsets: [13, 24],
|
|
commentOffsets: [10, 27],
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [28, 44],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Another case where the parsing heuristic says not to bother; note
|
|
// that there is no ";" in the comment.
|
|
{
|
|
parseComments: true,
|
|
input: "width: 5; /* background: yellow */ background: red;",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "5",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "width: 5;",
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "yellow",
|
|
priority: "",
|
|
offsets: [13, 31],
|
|
declarationText: "background: yellow",
|
|
commentOffsets: [10, 34],
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [35, 51],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Parsing a comment should yield text that has been unescaped, and
|
|
// the offsets should refer to the original text.
|
|
{
|
|
parseComments: true,
|
|
input: "/* content: '*\\/'; */",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "'*/'",
|
|
priority: "",
|
|
offsets: [3, 18],
|
|
declarationText: "content: '*\\/';",
|
|
commentOffsets: [0, 21],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Parsing a comment should yield text that has been unescaped, and
|
|
// the offsets should refer to the original text. This variant
|
|
// tests the no-semicolon path.
|
|
{
|
|
parseComments: true,
|
|
input: "/* content: '*\\/' */",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "'*/'",
|
|
priority: "",
|
|
offsets: [3, 17],
|
|
declarationText: "content: '*\\/'",
|
|
commentOffsets: [0, 20],
|
|
},
|
|
],
|
|
},
|
|
|
|
// A comment-in-a-comment should yield the correct offsets.
|
|
{
|
|
parseComments: true,
|
|
input: "/* color: /\\* comment *\\/ red; */",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [3, 30],
|
|
declarationText: "color: /\\* comment *\\/ red;",
|
|
commentOffsets: [0, 33],
|
|
},
|
|
],
|
|
},
|
|
|
|
// HTML comments are not special -- they are just ordinary tokens.
|
|
{
|
|
parseComments: true,
|
|
input: "<!-- color: red; --> color: blue;",
|
|
expected: [
|
|
{ name: "<!-- color", value: "red", priority: "", offsets: [0, 16] },
|
|
{ name: "--> color", value: "blue", priority: "", offsets: [17, 33] },
|
|
],
|
|
},
|
|
|
|
// Don't error on an empty comment.
|
|
{
|
|
parseComments: true,
|
|
input: "/**/",
|
|
expected: [],
|
|
},
|
|
|
|
// Parsing our special comments skips the name-check heuristic.
|
|
{
|
|
parseComments: true,
|
|
input: "/*! walrus: zebra; */",
|
|
expected: [
|
|
{
|
|
name: "walrus",
|
|
value: "zebra",
|
|
priority: "",
|
|
offsets: [4, 18],
|
|
declarationText: "walrus: zebra;",
|
|
commentOffsets: [0, 21],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Regression test for bug 1287620.
|
|
{
|
|
input: "color: blue \\9 no\\_need",
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "blue \\9 no\\_need",
|
|
priority: "",
|
|
offsets: [0, 23],
|
|
declarationText: "color: blue \\9 no\\_need",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Regression test for bug 1297890 - don't paste tokens.
|
|
{
|
|
parseComments: true,
|
|
input: "stroke-dasharray: 1/*ThisIsAComment*/2;",
|
|
expected: [
|
|
{
|
|
name: "stroke-dasharray",
|
|
value: "1 2",
|
|
priority: "",
|
|
offsets: [0, 39],
|
|
declarationText: "stroke-dasharray: 1/*ThisIsAComment*/2;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Regression test for bug 1384463 - don't trim significant
|
|
// whitespace.
|
|
{
|
|
// \u00a0 is non-breaking space.
|
|
input: "\u00a0vertical-align: top",
|
|
expected: [
|
|
{
|
|
name: "\u00a0vertical-align",
|
|
value: "top",
|
|
priority: "",
|
|
offsets: [0, 20],
|
|
declarationText: "\u00a0vertical-align: top",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Regression test for bug 1544223 - take CSS blocks into consideration before handling
|
|
// ; and : (i.e. don't advance to the property name or value automatically).
|
|
{
|
|
input: `--foo: (
|
|
:doodle {
|
|
@grid: 30x1 / 18vmin;
|
|
}
|
|
:container {
|
|
perspective: 30vmin;
|
|
}
|
|
|
|
@place-cell: center;
|
|
@size: 100%;
|
|
|
|
:after, :before {
|
|
content: '';
|
|
@size: 100%;
|
|
position: absolute;
|
|
border: 2.4vmin solid var(--color);
|
|
border-image: radial-gradient(
|
|
var(--color), transparent 60%
|
|
);
|
|
border-image-width: 4;
|
|
transform: rotate(@pn(0, 5deg));
|
|
}
|
|
|
|
will-change: transform, opacity;
|
|
animation: scale-up 15s linear infinite;
|
|
animation-delay: calc(-15s / @size() * @i());
|
|
box-shadow: inset 0 0 1em var(--color);
|
|
border-radius: 50%;
|
|
filter: var(--filter);
|
|
|
|
@keyframes scale-up {
|
|
0%, 100% {
|
|
transform: translateZ(0) scale(0) rotate(0);
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
99% {
|
|
transform:
|
|
translateZ(30vmin)
|
|
scale(1)
|
|
rotate(-270deg);
|
|
}
|
|
}
|
|
);`,
|
|
expected: [
|
|
{
|
|
name: "--foo",
|
|
value:
|
|
"( :doodle { @grid: 30x1 / 18vmin; } :container { perspective: 30vmin; } " +
|
|
"@place-cell: center; @size: 100%; :after, :before { content: ''; @size: " +
|
|
"100%; position: absolute; border: 2.4vmin solid var(--color); " +
|
|
"border-image: radial-gradient( var(--color), transparent 60% ); " +
|
|
"border-image-width: 4; transform: rotate(@pn(0, 5deg)); } will-change: " +
|
|
"transform, opacity; animation: scale-up 15s linear infinite; " +
|
|
"animation-delay: calc(-15s / @size() * @i()); box-shadow: inset 0 0 1em " +
|
|
"var(--color); border-radius: 50%; filter: var(--filter); @keyframes " +
|
|
"scale-up { 0%, 100% { transform: translateZ(0) scale(0) rotate(0); " +
|
|
"opacity: 0; } 50% { opacity: 1; } 99% { transform: translateZ(30vmin) " +
|
|
"scale(1) rotate(-270deg); } } )",
|
|
priority: "",
|
|
offsets: [0, 1036],
|
|
},
|
|
],
|
|
},
|
|
|
|
/***** Testing nested rules *****/
|
|
|
|
// Testing basic nesting with tagname selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with tagname + pseudo selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
div:hover {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with id selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
#myEl {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with class selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
.myEl {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with & + ident selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
& div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with direct child selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
> div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with & and :hover pseudo-class selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
&:hover {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with :not pseudo-class selector and non-leading &
|
|
{
|
|
input: `
|
|
color: red;
|
|
:not(&) {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with attribute selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
[class] {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with & compound selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
&div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with relative + selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
+ div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with relative ~ selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
~ div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting with relative * selector
|
|
{
|
|
input: `
|
|
color: red;
|
|
* div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting after a property with !important
|
|
{
|
|
input: `
|
|
color: red !important;
|
|
& div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "important",
|
|
offsets: [7, 29],
|
|
declarationText: "color: red !important;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing basic nesting after a comment
|
|
{
|
|
input: `
|
|
color: red;
|
|
/* nested rules */
|
|
& div {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing at-rules (with condition) nesting
|
|
{
|
|
input: `
|
|
color: red;
|
|
@media (orientation: landscape) {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing at-rules (without condition) nesting
|
|
{
|
|
input: `
|
|
color: red;
|
|
@media screen {
|
|
background: blue;
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing multi-level nesting
|
|
{
|
|
input: `
|
|
color: red;
|
|
&div {
|
|
&.active {
|
|
border: 1px;
|
|
}
|
|
padding: 10px;
|
|
}
|
|
background: gold;
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing multi-level nesting with at-rules
|
|
{
|
|
input: `
|
|
color: red;
|
|
@layer {
|
|
background: yellow;
|
|
@media screen {
|
|
& {
|
|
border-color: blue;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing sibling nested rules
|
|
{
|
|
input: `
|
|
color: red;
|
|
.active {
|
|
border: 1px;
|
|
}
|
|
&div {
|
|
padding: 10px;
|
|
}
|
|
border-color: cyan
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing nesting interwined between property declarations
|
|
{
|
|
input: `
|
|
color: red;
|
|
.active {
|
|
border: 1px;
|
|
}
|
|
background: gold;
|
|
&div {
|
|
padding: 10px;
|
|
}
|
|
border-color: cyan
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing that "}" in content property does not impact the nested state
|
|
{
|
|
input: `
|
|
color: red;
|
|
&div {
|
|
content: "}"
|
|
color: blue;
|
|
}
|
|
background: gold;
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing that "}" in attribute selector does not impact the nested state
|
|
{
|
|
input: `
|
|
color: red;
|
|
+ .foo {
|
|
[class="}"] {
|
|
padding: 10px;
|
|
}
|
|
}
|
|
background: gold;
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing that in function does not impact the nested state
|
|
{
|
|
input: `
|
|
color: red;
|
|
+ .foo {
|
|
background: url("img.png?x=}")
|
|
}
|
|
background: gold;
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing that "}" in comment does not impact the nested state
|
|
{
|
|
input: `
|
|
color: red;
|
|
+ .foo {
|
|
/* Check } */
|
|
padding: 10px;
|
|
}
|
|
background: gold;
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing that nested rules in comments aren't reported
|
|
{
|
|
parseComments: true,
|
|
input: "width: 5; /* div { color: cyan; } */ background: red;",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "5",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "width: 5;",
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [37, 53],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing that declarations in comments are still handled while nested rule in same comment is ignored
|
|
{
|
|
parseComments: true,
|
|
input:
|
|
"width: 5; /* padding: 12px; div { color: cyan; } margin: 1em; */ background: red;",
|
|
expected: [
|
|
{
|
|
name: "width",
|
|
value: "5",
|
|
priority: "",
|
|
offsets: [0, 9],
|
|
declarationText: "width: 5;",
|
|
},
|
|
{
|
|
name: "padding",
|
|
value: "12px",
|
|
priority: "",
|
|
offsets: [13, 27],
|
|
declarationText: "padding: 12px;",
|
|
},
|
|
{
|
|
name: "margin",
|
|
value: "1em",
|
|
priority: "",
|
|
offsets: [49, 61],
|
|
declarationText: "margin: 1em;",
|
|
},
|
|
{
|
|
name: "background",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [65, 81],
|
|
declarationText: "background: red;",
|
|
},
|
|
],
|
|
},
|
|
|
|
// Testing nesting without closing bracket
|
|
{
|
|
input: `
|
|
color: red;
|
|
& div {
|
|
background: blue;
|
|
`,
|
|
expected: [
|
|
{
|
|
name: "color",
|
|
value: "red",
|
|
priority: "",
|
|
offsets: [7, 18],
|
|
declarationText: "color: red;",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
function run_test() {
|
|
run_basic_tests();
|
|
run_comment_tests();
|
|
run_named_tests();
|
|
}
|
|
|
|
// Test parseDeclarations.
|
|
function run_basic_tests() {
|
|
for (const test of TEST_DATA) {
|
|
info("Test input string " + test.input);
|
|
let output;
|
|
try {
|
|
output = parseDeclarations(
|
|
isCssPropertyKnown,
|
|
test.input,
|
|
test.parseComments
|
|
);
|
|
} catch (e) {
|
|
info(
|
|
"parseDeclarations threw an exception with the given input " + "string"
|
|
);
|
|
if (test.throws) {
|
|
info("Exception expected");
|
|
Assert.ok(true);
|
|
} else {
|
|
info("Exception unexpected\n" + e);
|
|
Assert.ok(false);
|
|
}
|
|
}
|
|
if (output) {
|
|
assertOutput(test.input, output, test.expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
const COMMENT_DATA = [
|
|
{
|
|
input: "content: 'hi",
|
|
expected: [
|
|
{
|
|
name: "content",
|
|
value: "'hi",
|
|
priority: "",
|
|
terminator: "';",
|
|
offsets: [2, 14],
|
|
colonOffsets: [9, 11],
|
|
commentOffsets: [0, 16],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
input: "text that once confounded the parser;",
|
|
expected: [],
|
|
},
|
|
];
|
|
|
|
// Test parseCommentDeclarations.
|
|
function run_comment_tests() {
|
|
for (const test of COMMENT_DATA) {
|
|
info("Test input string " + test.input);
|
|
const output = _parseCommentDeclarations(
|
|
isCssPropertyKnown,
|
|
test.input,
|
|
0,
|
|
test.input.length + 4
|
|
);
|
|
deepEqual(output, test.expected);
|
|
}
|
|
}
|
|
|
|
const NAMED_DATA = [
|
|
{
|
|
input: "position:absolute;top50px;height:50px;",
|
|
expected: [
|
|
{
|
|
name: "position",
|
|
value: "absolute",
|
|
priority: "",
|
|
terminator: "",
|
|
offsets: [0, 18],
|
|
colonOffsets: [8, 9],
|
|
},
|
|
{
|
|
name: "height",
|
|
value: "50px",
|
|
priority: "",
|
|
terminator: "",
|
|
offsets: [26, 38],
|
|
colonOffsets: [32, 33],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// Test parseNamedDeclarations.
|
|
function run_named_tests() {
|
|
for (const test of NAMED_DATA) {
|
|
info("Test input string " + test.input);
|
|
const output = parseNamedDeclarations(isCssPropertyKnown, test.input, true);
|
|
info(JSON.stringify(output));
|
|
deepEqual(output, test.expected);
|
|
}
|
|
}
|
|
|
|
function assertOutput(input, actual, expected) {
|
|
if (actual.length === expected.length) {
|
|
for (let i = 0; i < expected.length; i++) {
|
|
Assert.ok(!!actual[i]);
|
|
info(
|
|
"Check that the output item has the expected name, " +
|
|
"value and priority"
|
|
);
|
|
Assert.equal(actual[i].name, expected[i].name, "Expected name");
|
|
Assert.equal(actual[i].value, expected[i].value, "Expected value");
|
|
Assert.equal(
|
|
actual[i].priority,
|
|
expected[i].priority,
|
|
"Expected priority"
|
|
);
|
|
deepEqual(actual[i].offsets, expected[i].offsets, "Expected offsets");
|
|
if ("commentOffsets" in expected[i]) {
|
|
deepEqual(
|
|
actual[i].commentOffsets,
|
|
expected[i].commentOffsets,
|
|
"Expected commentOffsets"
|
|
);
|
|
}
|
|
|
|
if (expected[i].declarationText) {
|
|
Assert.equal(
|
|
input.substring(expected[i].offsets[0], expected[i].offsets[1]),
|
|
expected[i].declarationText,
|
|
"Expected declarationText"
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
for (const prop of actual) {
|
|
info(
|
|
"Actual output contained: {name: " +
|
|
prop.name +
|
|
", value: " +
|
|
prop.value +
|
|
", priority: " +
|
|
prop.priority +
|
|
"}"
|
|
);
|
|
}
|
|
Assert.equal(actual.length, expected.length);
|
|
}
|
|
}
|