diff --git a/grammars/css.cson b/grammars/css.cson index 6256e55..d7a496d 100644 --- a/grammars/css.cson +++ b/grammars/css.cson @@ -23,7 +23,7 @@ 'include': '#escapes' } { - 'include': '#combinators' + 'include': '#combinator-invalid' } { 'include': '#selector' @@ -36,6 +36,9 @@ } ] 'repository': + 'arithmetic-operators': + 'match': '[*/]|(?<=\\s|^)[-+](?=\\s|$)' + 'name': 'keyword.operator.arithmetic.css' 'at-rules': 'patterns': [ { @@ -214,6 +217,9 @@ 'name': 'punctuation.section.media.end.bracket.curly.css' 'name': 'meta.at-rule.media.body.css' 'patterns': [ + { + 'include': '#rule-list-innards' + } { 'include': '$self' } @@ -632,6 +638,9 @@ 'name': 'punctuation.section.end.bracket.curly.css' 'name': 'meta.at-rule.body.css' 'patterns': [ + { + 'include': '#rule-list-innards' + } { 'include': '$self' } @@ -643,12 +652,12 @@ 'color-keywords': 'patterns': [ { - # CSS 2.1 colours: http://www.w3.org/TR/CSS21/syndata.html#value-def-color + # CSS 2.1 colors: http://www.w3.org/TR/CSS21/syndata.html#value-def-color 'match': '(?i)(?>>' - 'name': 'invalid.deprecated.combinator.css' + 'include': '#combinator-invalid' } { 'match': '>>|>|\\+|~' 'name': 'keyword.operator.combinator.css' } ] + 'combinator-invalid': + 'match': '/deep/|>>>' + 'name': 'invalid.deprecated.combinator.css' 'commas': 'match': ',' 'name': 'punctuation.separator.list.comma.css' @@ -818,15 +829,17 @@ 'name': 'meta.function.calc.css' 'patterns': [ { - 'match': '[*/]|(?<=\\s|^)[-+](?=\\s|$)' - 'name': 'keyword.operator.arithmetic.css' + 'include': '#arithmetic-operators' } { 'include': '#property-values' } + { + 'include': '#function-nesting' + } ] } - # Colours + # Colors { 'begin': '(?i)(?~+] # Selector combinator + | \\| # Selector namespace separator + | \\[ # Attribute selector opening bracket + | [a-zA-Z] # Letter + | [^\\x00-\\x7F] # Non-ASCII symbols + | \\\\(?:[0-9a-fA-F]{1,6}|.) # Escape sequence + + # Or one of the following symbols, followed by a word character, hyphen, or escape sequence: + | (?: + \\. # Class selector + | \\# # ID selector + ) + (?: + [\\w-] # Word character or hyphen + | \\\\(?: # Escape sequence + [0-9a-fA-F]{1,6} + | . + ) + ) + + # Or one of the following symbols, followed a letter or hyphen: + | (?: + \\: # Pseudo-class + | \\:\\: # Pseudo-element + ) + [a-zA-Z-] # Letter or hyphen ) ) - ''' - 'end': '(?=\\s*[/@{)])' + + # Match must NOT contain any of the following: + (?! + [\\w-]*[\\:]+\\s # One or more colons immediately followed by a whitespace (denotes a property or invalid sequence) + | [^{]*; # Any characters and a semicolon before an opening bracket (denotes the end of a property) + | [^{]*} # A closing bracket before an opening bracket (denotes a property) + | [^\\:]+(\\:\\:):+ # More than two colons (invalid sequence) + ) + ''' + 'end': '''(?x) + # Match must end with: + (?= + \\s* # Optional whitespace and one of the following: + (?: + \\/ # Comment + | @ # At-rule + | { # Opening property list brace + | \\) # Closing function brace (for passing test on `some-edgy-new-function(`) + | $ # End of line + ) + ) + ''' 'name': 'meta.selector.css' 'patterns': [ { @@ -1790,7 +1932,7 @@ [-\\w*]+ \\| (?! - [-\\[:.*\\#a-zA-Z_] # Make sure there's a selector to match + [-\\[:.*&\\#a-zA-Z_] # Make sure there's a selector to match | [^\\x00-\\x7F] ) ) @@ -1808,6 +1950,10 @@ { 'include': '#tag-names' } + { + 'match': '&' + 'name': 'entity.name.tag.nesting.css' + } { 'match': '\\*' 'name': 'entity.name.tag.wildcard.css' @@ -1833,7 +1979,7 @@ # Consists of a hyphen only - # Terminated by either: (?= $ # - End-of-line - | [\\s,.\\#)\\[:{>+~|] # - Followed by another selector + | [\\s,.\\#)\\[:{>+~|&] # - Followed by another selector | /\\* # - Followed by a block comment ) | @@ -1843,9 +1989,11 @@ | \\\\(?:[0-9a-fA-F]{1,6}|.) # - Escape sequence )* (?: # Invalid punctuation - [!"'%&(*;+~|] # - Another selector + | [\\s,.\\#)\\[:{>+~|&] # - Another selector | /\\* # - A block comment ) ''' @@ -1897,7 +2045,7 @@ (?![0-9]) (?:[-a-zA-Z0-9_]|[^\\x00-\\x7F]|\\\\(?:[0-9a-fA-F]{1,6}|.))+ ) - (?=$|[\\s,.\\#)\\[:{>+~|]|/\\*) + (?=$|[\\s,.\\#)\\[:{>+~|&]|/\\*) ''' 'name': 'entity.other.attribute-name.id.css' } @@ -2012,6 +2160,135 @@ 'name': 'entity.name.tag.custom.css' } ] + 'shared-names': + # Shared names are keywords that are listed in both selector and property name contexts. + 'patterns': [ + # The following are considered selectors by default: + { + 'begin': '''(?xi) + # Selector match must be preceded by one of the following: + (?<= + ^ # Start of line + | (^|[^\\:])\\s # Whitespace, after the start of a line or any character except a colon + | [{}] # Opening or closing brace (condensed property list syntax) + | \\*/ # Comment end + | \\\\(?:[0-9a-fA-F]{1,6}|.) # Escape sequence + ) + + (?= + # Selector must match: + (?: + # HTML elements + header|image|label|marquee|mask|nav|ruby|shadow|span|style + # SVG elements + |color-profile|line|text + ) + + # Selector must NOT be followed by any any of the following: + (?! + .*; # Any characters followed by a semicolon (denotes a property) + | [^{]*} # Any characters, except an opening brace, followed by a closing bracket (denotes a property) + | - # A dash (denotes a property name) + | \\:\\s+ # A colon followed by whitespace (denotes a property name) + ) + ) + ''' + 'end': '''(?xi) + # Selector match ends with one of the following: + (?= + \\s # Whitespace + | \\/\\* # Comment + | , # Comma + | { # Opening property list brace + | $ # End of line + ) + ''' + 'patterns': [ + { + 'include': '#selector' + } + ] + } + # The following are considered property names by default when attached to a colon: + { + 'begin': '''(?xi) + # Selector match must be preceded by one of the following: + (?<= + ^ # Start of line + | (^|[^\\:])\\s # Whitespace, after the start of a line or any character except a colon + | [{}] # Opening or closing brace (condensed property list syntax) + | \\*/ # Comment end + | \\\\(?:[0-9a-fA-F]{1,6}|.) # Escape sequence + ) + + (?= + # Selector must match: + (?: + # HTML elements + content|font|mark + # SVG elements + |cursor|filter + ) + + # Selector must NOT be followed by any any of the following: + (?! + .*; # Any characters followed by a semicolon (denotes a property) + | [^{]*} # Any characters, except an opening brace, followed by a closing bracket (denotes a property) + | - # A dash (denotes a property name) + | \\: # A colon, unless it's with one of the following: + (?! + # An opening bracket before a closing bracket + [^}]*{ + # A pseudo-class selectors + | active|any-link|checked|disabled|empty|enabled|first + | (?:first|last|only)-(?:child|of-type)|focus|focus-visible|focus-within|fullscreen|host|hover + | in-range|indeterminate|invalid|link|out-of-range + | read-only|read-write|required|root|scope|target|unresolved + | valid|visited + + # A functional pseudo-class selectors + | (?: dir|lang + | not|has|matches|where|is + | nth-(?:last-)?(?:child|of-type) + )\\( + + # A single-colon pseudo-element selectors + | after + | before + | first-letter + | first-line + | (?: + \\- + (?: + ah|apple|atsc|epub|hp|khtml|moz + | ms|o|rim|ro|tc|wap|webkit|xv + ) + | (?: + mso|prince + ) + ) + -[a-z-]+ + ) + ) + ) + ''' + 'end': '''(?xi) + # Selector match ends with one of the following: + (?= + \\s # Whitespace + | \\/\\* # Comment + | , # Comma + | { # Opening property list brace + | $ # End of line + ) + ''' + 'patterns': [ + { + 'include': '#selector' + } + ] + } + ] 'string': 'patterns': [ { @@ -2100,7 +2377,7 @@ | mrow|ms|mscarries|mscarry|msgroup|msline|mspace|msqrt|msrow|mstack|mstyle|msub|msubsup | msup|mtable|mtd|mtext|mtr|munder|munderover|semantics ) - (?=[+~>\\s,.\\#|){:\\[]|/\\*|$) + (?=[+~>\\s,.\\#|&){:\\[]|/\\*|$) ''' 'name': 'entity.name.tag.css' 'unicode-range': diff --git a/spec/css-spec.mjs b/spec/css-spec.mjs index 9086ed1..0d3fcd9 100644 --- a/spec/css-spec.mjs +++ b/spec/css-spec.mjs @@ -3690,4 +3690,115 @@ describe('CSS grammar', function () { assert.deepStrictEqual(tokens[0][11], { scopes: ['source.css', 'meta.property-list.css', 'punctuation.section.property-list.end.bracket.curly.css'], value: '}' }); }); }); + + describe('CSS Nesting', function () { + it('tokenizes the nesting selector &', function () { + var tokens; + tokens = testGrammar.tokenizeLine('& {}').tokens; + assert.deepStrictEqual(tokens[0], { scopes: ['source.css', 'meta.selector.css', 'entity.name.tag.nesting.css'], value: '&' }); + }); + + it('tokenizes the nesting selector & with class', function () { + var tokens; + tokens = testGrammar.tokenizeLine('&.foo {}').tokens; + assert.deepStrictEqual(tokens[0], { scopes: ['source.css', 'meta.selector.css', 'entity.name.tag.nesting.css'], value: '&' }); + assert.deepStrictEqual(tokens[1], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.class.css', 'punctuation.definition.entity.css'], value: '.' }); + assert.deepStrictEqual(tokens[2], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.class.css'], value: 'foo' }); + }); + + it('tokenizes the nesting selector & with pseudo-class', function () { + var tokens; + tokens = testGrammar.tokenizeLine('&:hover {}').tokens; + assert.deepStrictEqual(tokens[0], { scopes: ['source.css', 'meta.selector.css', 'entity.name.tag.nesting.css'], value: '&' }); + assert.deepStrictEqual(tokens[1], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.pseudo-class.css', 'punctuation.definition.entity.css'], value: ':' }); + assert.deepStrictEqual(tokens[2], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.pseudo-class.css'], value: 'hover' }); + }); + + it('tokenizes the nesting selector & inside a rule', function () { + var tokens; + tokens = testGrammar.tokenizeLine(' & > .bar {}').tokens; + assert.deepStrictEqual(tokens[1], { scopes: ['source.css', 'meta.selector.css', 'entity.name.tag.nesting.css'], value: '&' }); + assert.deepStrictEqual(tokens[3], { scopes: ['source.css', 'meta.selector.css', 'keyword.operator.combinator.css'], value: '>' }); + }); + + it('tokenizes nested selector with suffix &', function () { + var lines; + lines = testGrammar.tokenizeLines('.foo::before {\n content: "Hello";\n\n .important & {\n color: red;\n }\n}'); + assert.deepStrictEqual(lines[0][0], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.class.css', 'punctuation.definition.entity.css'], value: '.' }); + assert.deepStrictEqual(lines[0][1], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.class.css'], value: 'foo' }); + assert.deepStrictEqual(lines[0][2], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.pseudo-element.css', 'punctuation.definition.entity.css'], value: '::' }); + assert.deepStrictEqual(lines[0][3], { scopes: ['source.css', 'meta.selector.css', 'entity.other.attribute-name.pseudo-element.css'], value: 'before' }); + + assert.deepStrictEqual(lines[3][1], { scopes: ['source.css', 'meta.property-list.css', 'meta.selector.css', 'entity.other.attribute-name.class.css', 'punctuation.definition.entity.css'], value: '.' }); + assert.deepStrictEqual(lines[3][2], { scopes: ['source.css', 'meta.property-list.css', 'meta.selector.css', 'entity.other.attribute-name.class.css'], value: 'important' }); + assert.deepStrictEqual(lines[3][4], { scopes: ['source.css', 'meta.property-list.css', 'meta.selector.css', 'entity.name.tag.nesting.css'], value: '&' }); + }); + }); + + describe('advanced property values', function () { + it('tokenizes json-like structures in custom properties', function () { + var tokens; + tokens = testGrammar.tokenizeLine('.foo { --json: { "foo": "bar" }; }').tokens; + assert.deepStrictEqual(tokens[5], { scopes: ['source.css', 'meta.property-list.css', 'variable.css'], value: '--json' }); + assert.deepStrictEqual(tokens[6], { scopes: ['source.css', 'meta.property-list.css', 'punctuation.separator.key-value.css'], value: ':' }); + assert.deepStrictEqual(tokens[8], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'punctuation.section.group.begin.bracket.curly.css'], value: '{' }); + assert.deepStrictEqual(tokens[10], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css', 'punctuation.definition.string.begin.css'], value: '"' }); + assert.deepStrictEqual(tokens[11], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css'], value: 'foo' }); + assert.deepStrictEqual(tokens[12], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css', 'punctuation.definition.string.end.css'], value: '"' }); + assert.deepStrictEqual(tokens[13], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css'], value: ': ' }); + assert.deepStrictEqual(tokens[14], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css', 'punctuation.definition.string.begin.css'], value: '"' }); + assert.deepStrictEqual(tokens[15], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css'], value: 'bar' }); + assert.deepStrictEqual(tokens[16], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css', 'punctuation.definition.string.end.css'], value: '"' }); + assert.deepStrictEqual(tokens[18], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'punctuation.section.group.end.bracket.curly.css'], value: '}' }); + }); + + it('tokenizes json-like structures with boolean values', function () { + var tokens; + tokens = testGrammar.tokenizeLine(':root { --foo: { "bar": true }; }').tokens; + assert.deepStrictEqual(tokens[5], { scopes: ['source.css', 'meta.property-list.css', 'variable.css'], value: '--foo' }); + assert.deepStrictEqual(tokens[6], { scopes: ['source.css', 'meta.property-list.css', 'punctuation.separator.key-value.css'], value: ':' }); + assert.deepStrictEqual(tokens[8], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'punctuation.section.group.begin.bracket.curly.css'], value: '{' }); + assert.deepStrictEqual(tokens[10], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css', 'punctuation.definition.string.begin.css'], value: '"' }); + assert.deepStrictEqual(tokens[11], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css'], value: 'bar' }); + assert.deepStrictEqual(tokens[12], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'string.quoted.double.css', 'punctuation.definition.string.end.css'], value: '"' }); + assert.deepStrictEqual(tokens[13], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css'], value: ': true ' }); + assert.deepStrictEqual(tokens[14], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'punctuation.section.group.end.bracket.curly.css'], value: '}' }); + }); + + it('tokenizes curly braces in function arguments', function () { + var tokens; + tokens = testGrammar.tokenizeLine('.foo { color: --foo({1, 2, 3}, 4); }').tokens; + assert.deepStrictEqual(tokens[5], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-name.css', 'support.type.property-name.css'], value: 'color' }); + assert.deepStrictEqual(tokens[6], { scopes: ['source.css', 'meta.property-list.css', 'punctuation.separator.key-value.css'], value: ':' }); + assert.deepStrictEqual(tokens[8], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'support.function.custom.css'], value: '--foo' }); + assert.deepStrictEqual(tokens[9], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.section.function.begin.bracket.round.css'], value: '(' }); + assert.deepStrictEqual(tokens[10], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.section.group.begin.bracket.curly.css'], value: '{' }); + assert.deepStrictEqual(tokens[11], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'constant.numeric.css'], value: '1' }); + assert.deepStrictEqual(tokens[12], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.separator.list.comma.css'], value: ',' }); + assert.deepStrictEqual(tokens[14], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'constant.numeric.css'], value: '2' }); + assert.deepStrictEqual(tokens[15], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.separator.list.comma.css'], value: ',' }); + assert.deepStrictEqual(tokens[17], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'constant.numeric.css'], value: '3' }); + assert.deepStrictEqual(tokens[18], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.section.group.end.bracket.curly.css'], value: '}' }); + assert.deepStrictEqual(tokens[19], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.separator.list.comma.css'], value: ',' }); + assert.deepStrictEqual(tokens[21], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'constant.numeric.css'], value: '4' }); + assert.deepStrictEqual(tokens[22], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.custom.css', 'punctuation.section.function.end.bracket.round.css'], value: ')' }); + }); + + it('tokenizes if() function', function () { + var tokens; + tokens = testGrammar.tokenizeLine('.foo { color: if(style(--foo: 1), red, blue); }').tokens; + assert.deepStrictEqual(tokens[8], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'support.function.misc.css'], value: 'if' }); + assert.deepStrictEqual(tokens[9], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'punctuation.section.function.begin.bracket.round.css'], value: '(' }); + assert.deepStrictEqual(tokens[10], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'meta.function.misc.css', 'support.function.misc.css'], value: 'style' }); + assert.deepStrictEqual(tokens[11], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'meta.function.misc.css', 'punctuation.section.function.begin.bracket.round.css'], value: '(' }); + assert.deepStrictEqual(tokens[12], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'meta.function.misc.css', 'variable.parameter.misc.css'], value: '--foo:' }); + assert.deepStrictEqual(tokens[14], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'meta.function.misc.css', 'constant.numeric.css'], value: '1' }); + assert.deepStrictEqual(tokens[15], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'meta.function.misc.css', 'punctuation.section.function.end.bracket.round.css'], value: ')' }); + assert.deepStrictEqual(tokens[16], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'punctuation.separator.list.comma.css'], value: ',' }); + assert.deepStrictEqual(tokens[18], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'support.constant.color.w3c-standard-color-name.css'], value: 'red' }); + assert.deepStrictEqual(tokens[19], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'punctuation.separator.list.comma.css'], value: ',' }); + assert.deepStrictEqual(tokens[21], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'support.constant.color.w3c-standard-color-name.css'], value: 'blue' }); + assert.deepStrictEqual(tokens[22], { scopes: ['source.css', 'meta.property-list.css', 'meta.property-value.css', 'meta.function.misc.css', 'punctuation.section.function.end.bracket.round.css'], value: ')' }); + }); + }); });