mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 10:38:59 +01:00
Move SCSS to extension
This commit is contained in:
@@ -15,6 +15,9 @@ import {CSSNavigation} from './services/cssNavigation';
|
||||
import {CSSCodeActions} from './services/cssCodeActions';
|
||||
import {CSSValidation} from './services/cssValidation';
|
||||
|
||||
import {SCSSParser} from './parser/scssParser';
|
||||
import {SCSSCompletion} from './services/scssCompletion';
|
||||
|
||||
export interface LanguageService {
|
||||
configure(raw: LanguageSettings): void;
|
||||
doValidation(document: TextDocument, stylesheet: Stylesheet): Thenable<Diagnostic[]>;
|
||||
@@ -34,17 +37,18 @@ export interface LanguageSettings {
|
||||
lint?: any;
|
||||
}
|
||||
|
||||
export function getCSSLanguageService() : LanguageService {
|
||||
let parser = new Parser();
|
||||
let cssParser = new Parser();
|
||||
let cssCompletion = new CSSCompletion();
|
||||
let cssHover = new CSSHover();
|
||||
let cssValidation = new CSSValidation();
|
||||
let cssNavigation = new CSSNavigation();
|
||||
let cssCodeActions = new CSSCodeActions();
|
||||
|
||||
export function getCSSLanguageService() : LanguageService {
|
||||
return {
|
||||
configure: cssValidation.configure.bind(cssValidation),
|
||||
doValidation: cssValidation.doValidation.bind(cssValidation),
|
||||
parseStylesheet: parser.parseStylesheet.bind(parser),
|
||||
parseStylesheet: cssParser.parseStylesheet.bind(cssParser),
|
||||
doComplete: cssCompletion.doComplete.bind(cssCompletion),
|
||||
doHover: cssHover.doHover.bind(cssHover),
|
||||
findDefinition: cssNavigation.findDefinition.bind(cssNavigation),
|
||||
@@ -54,4 +58,14 @@ export function getCSSLanguageService() : LanguageService {
|
||||
doCodeActions: cssCodeActions.doCodeActions.bind(cssCodeActions),
|
||||
findColorSymbols: cssNavigation.findColorSymbols.bind(cssNavigation)
|
||||
};
|
||||
}
|
||||
|
||||
let scssParser = new SCSSParser();
|
||||
let scssCompletion = new SCSSCompletion();
|
||||
|
||||
export function getSCSSLanguageService() : LanguageService {
|
||||
let languageService = getCSSLanguageService();
|
||||
languageService.parseStylesheet = scssParser.parseStylesheet.bind(scssParser);
|
||||
languageService.doComplete = scssCompletion.doComplete.bind(scssCompletion);
|
||||
return languageService;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TextDocuments, TextDocument, InitializeParams, InitializeResult, RequestType
|
||||
} from 'vscode-languageserver';
|
||||
|
||||
import {getCSSLanguageService, LanguageSettings, LanguageService} from './cssLanguageService';
|
||||
import {getCSSLanguageService, getSCSSLanguageService, LanguageSettings, LanguageService} from './cssLanguageService';
|
||||
import {Stylesheet} from './parser/cssNodes';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
@@ -56,7 +56,8 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
});
|
||||
|
||||
let languageServices : { [id:string]: LanguageService} = {
|
||||
css: getCSSLanguageService()
|
||||
css: getCSSLanguageService(),
|
||||
scss: getSCSSLanguageService()
|
||||
};
|
||||
|
||||
function getLanguageService(document: TextDocument) {
|
||||
|
||||
26
extensions/css/server/src/parser/scssErrors.ts
Normal file
26
extensions/css/server/src/parser/scssErrors.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nodes from './cssNodes';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class SCSSIssueType implements nodes.IRule {
|
||||
id: string;
|
||||
message: string;
|
||||
|
||||
public constructor(id: string, message: string) {
|
||||
this.id = id;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
export var SCSSParseError = {
|
||||
FromExpected: new SCSSIssueType('sass-fromexpected', localize('expected.from', "'from' expected")),
|
||||
ThroughOrToExpected: new SCSSIssueType('sass-throughexpected', localize('expected.through', "'through' or 'to' expected")),
|
||||
InExpected: new SCSSIssueType('sass-fromexpected', localize('expected.in', "'in' expected")),
|
||||
};
|
||||
535
extensions/css/server/src/parser/scssParser.ts
Normal file
535
extensions/css/server/src/parser/scssParser.ts
Normal file
@@ -0,0 +1,535 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
import * as sassScanner from './scssScanner';
|
||||
import {TokenType} from './cssScanner';
|
||||
import * as cssParser from './cssParser';
|
||||
import * as nodes from './cssNodes';
|
||||
|
||||
import {SCSSParseError} from './scssErrors';
|
||||
import {ParseError} from './cssErrors';
|
||||
|
||||
/// <summary>
|
||||
/// A parser for Sass
|
||||
/// http://sass-lang.com/documentation/file.SASS_REFERENCE.html
|
||||
/// </summary>
|
||||
export class SCSSParser extends cssParser.Parser {
|
||||
|
||||
public constructor() {
|
||||
super(new sassScanner.SCSSScanner());
|
||||
}
|
||||
|
||||
public _parseStylesheetStatement(): nodes.Node {
|
||||
return super._parseStylesheetStatement()
|
||||
|| this._parseVariableDeclaration()
|
||||
|| this._parseWarnAndDebug()
|
||||
|| this._parseControlStatement()
|
||||
|| this._parseMixinDeclaration()
|
||||
|| this._parseMixinContent()
|
||||
|| this._parseMixinReference() // @include
|
||||
|| this._parseFunctionDeclaration();
|
||||
}
|
||||
|
||||
public _parseImport(): nodes.Node {
|
||||
let node = <nodes.Import>this.create(nodes.Import);
|
||||
if (!this.accept(TokenType.AtKeyword, '@import')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) {
|
||||
return this.finish(node, ParseError.URIOrStringExpected);
|
||||
}
|
||||
while (this.accept(TokenType.Comma)) {
|
||||
if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) {
|
||||
return this.finish(node, ParseError.URIOrStringExpected);
|
||||
}
|
||||
}
|
||||
|
||||
node.setMedialist(this._parseMediaList());
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
// Sass variables: $font-size: 12px;
|
||||
public _parseVariableDeclaration(panic: TokenType[] = []): nodes.VariableDeclaration {
|
||||
let node = <nodes.VariableDeclaration>this.create(nodes.VariableDeclaration);
|
||||
|
||||
if (!node.setVariable(this._parseVariable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.Colon, ':')) {
|
||||
return this.finish(node, ParseError.ColonExpected);
|
||||
}
|
||||
node.colonPosition = this.prevToken.offset;
|
||||
|
||||
if (!node.setValue(this._parseExpr())) {
|
||||
return this.finish(node, ParseError.VariableValueExpected, [], panic);
|
||||
}
|
||||
|
||||
if (this.accept(TokenType.Exclamation)) {
|
||||
if (!this.accept(TokenType.Ident, 'default', true)) {
|
||||
return this.finish(node, ParseError.UnknownKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.peek(TokenType.SemiColon)) {
|
||||
node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist
|
||||
}
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMediaFeatureName(): nodes.Node {
|
||||
return this._parseFunction() || this._parseIdent() || this._parseVariable(); // first function, the indent
|
||||
}
|
||||
|
||||
public _parseKeyframeSelector(): nodes.Node {
|
||||
return super._parseKeyframeSelector() || this._parseMixinContent();
|
||||
}
|
||||
|
||||
public _parseVariable(): nodes.Variable {
|
||||
let node = <nodes.Variable>this.create(nodes.Variable);
|
||||
if (!this.accept(sassScanner.VariableName)) {
|
||||
return null;
|
||||
}
|
||||
return <nodes.Variable>node;
|
||||
}
|
||||
|
||||
public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier {
|
||||
let node = <nodes.Identifier>this.create(nodes.Identifier);
|
||||
node.referenceTypes = referenceTypes;
|
||||
let hasContent = false;
|
||||
while (this.accept(TokenType.Ident) || node.addChild(this._parseInterpolation())) {
|
||||
hasContent = true;
|
||||
if (!this.hasWhitespace() && this.accept(TokenType.Delim, '-')) {
|
||||
// '-' is a valid char inside a ident (special treatment here to support #{foo}-#{bar})
|
||||
}
|
||||
if (this.hasWhitespace()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasContent ? this.finish(node) : null;
|
||||
}
|
||||
|
||||
public _parseTerm(): nodes.Term {
|
||||
let term = super._parseTerm();
|
||||
if (term) { return term; }
|
||||
|
||||
term = <nodes.Term>this.create(nodes.Term);
|
||||
if (term.setExpression(this._parseVariable())) {
|
||||
return <nodes.Term>this.finish(term);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseInterpolation(): nodes.Node {
|
||||
let node = this.create(nodes.Interpolation);
|
||||
if (this.accept(sassScanner.InterpolationFunction)) {
|
||||
if (!node.addChild(this._parseBinaryExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected);
|
||||
}
|
||||
if (!this.accept(TokenType.CurlyR)) {
|
||||
return this.finish(node, ParseError.RightCurlyExpected);
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseOperator(): nodes.Node {
|
||||
if (this.peek(sassScanner.EqualsOperator) || this.peek(sassScanner.NotEqualsOperator)
|
||||
|| this.peek(sassScanner.GreaterEqualsOperator) || this.peek(sassScanner.SmallerEqualsOperator)
|
||||
|| this.peek(TokenType.Delim, '>') || this.peek(TokenType.Delim, '<')
|
||||
|| this.peek(TokenType.Ident, 'and') || this.peek(TokenType.Ident, 'or')
|
||||
|| this.peek(TokenType.Delim, '%')
|
||||
) {
|
||||
let node = this.createNode(nodes.NodeType.Operator);
|
||||
this.consumeToken();
|
||||
return this.finish(node);
|
||||
}
|
||||
return super._parseOperator();
|
||||
}
|
||||
|
||||
public _parseUnaryOperator(): nodes.Node {
|
||||
if (this.peek(TokenType.Ident, 'not')) {
|
||||
let node = this.create(nodes.Node);
|
||||
this.consumeToken();
|
||||
return this.finish(node);
|
||||
}
|
||||
return super._parseUnaryOperator();
|
||||
}
|
||||
|
||||
public _parseRuleSetDeclaration(): nodes.Node {
|
||||
if (this.peek(TokenType.AtKeyword)) {
|
||||
return this._parseKeyframe() // nested @keyframe
|
||||
|| this._parseImport() // nested @import
|
||||
|| this._parseMedia() // nested @media
|
||||
|| this._parseFontFace() // nested @font-face
|
||||
|| this._parseWarnAndDebug() // @warn and @debug statements
|
||||
|| this._parseControlStatement() // @if, @while, @for, @each
|
||||
|| this._parseFunctionDeclaration() // @function
|
||||
|| this._parseExtends() // @extends
|
||||
|| this._parseMixinReference() // @include
|
||||
|| this._parseMixinContent() // @content
|
||||
|| this._parseMixinDeclaration(); // nested @mixin
|
||||
}
|
||||
return this._parseVariableDeclaration() // variable declaration
|
||||
|| this._tryParseRuleset(true) // nested ruleset
|
||||
|| super._parseRuleSetDeclaration(); // try css ruleset declaration as last so in the error case, the ast will contain a declaration
|
||||
}
|
||||
|
||||
public _parseDeclaration(resyncStopTokens?: TokenType[]): nodes.Declaration {
|
||||
let node = <nodes.Declaration>this.create(nodes.Declaration);
|
||||
if (!node.setProperty(this._parseProperty())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.Colon, ':')) {
|
||||
return this.finish(node, ParseError.ColonExpected, [TokenType.Colon], resyncStopTokens);
|
||||
}
|
||||
node.colonPosition = this.prevToken.offset;
|
||||
|
||||
let hasContent = false;
|
||||
if (node.setValue(this._parseExpr())) {
|
||||
hasContent = true;
|
||||
node.addChild(this._parsePrio());
|
||||
}
|
||||
if (this.peek(TokenType.CurlyL)) {
|
||||
node.setNestedProperties(this._parseNestedProperties());
|
||||
} else {
|
||||
if (!hasContent) {
|
||||
return this.finish(node, ParseError.PropertyValueExpected);
|
||||
}
|
||||
}
|
||||
if (this.peek(TokenType.SemiColon)) {
|
||||
node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseNestedProperties(): nodes.NestedProperties {
|
||||
let node = <nodes.NestedProperties>this.create(nodes.NestedProperties);
|
||||
return this._parseBody(node, this._parseDeclaration.bind(this));
|
||||
}
|
||||
|
||||
public _parseExtends(): nodes.Node {
|
||||
let node = <nodes.ExtendsReference>this.create(nodes.ExtendsReference);
|
||||
if (this.accept(TokenType.AtKeyword, '@extend')) {
|
||||
if (!node.setSelector(this._parseSimpleSelector())) {
|
||||
return this.finish(node, ParseError.SelectorExpected);
|
||||
}
|
||||
if (this.accept(TokenType.Exclamation)) {
|
||||
if (!this.accept(TokenType.Ident, 'optional', true)) {
|
||||
return this.finish(node, ParseError.UnknownKeyword);
|
||||
}
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseSimpleSelectorBody(): nodes.Node {
|
||||
return this._parseSelectorCombinator() || this._parseSelectorPlaceholder() || super._parseSimpleSelectorBody();
|
||||
}
|
||||
|
||||
public _parseSelectorCombinator(): nodes.Node {
|
||||
let node = this.createNode(nodes.NodeType.SelectorCombinator);
|
||||
if (this.accept(TokenType.Delim, '&')) {
|
||||
while (!this.hasWhitespace() && (this.accept(TokenType.Delim, '-') || node.addChild(this._parseIdent()) || this.accept(TokenType.Delim, '&'))) {
|
||||
// support &-foo
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseSelectorPlaceholder(): nodes.Node {
|
||||
let node = this.createNode(nodes.NodeType.SelectorPlaceholder);
|
||||
if (this.accept(TokenType.Delim, '%')) {
|
||||
this._parseIdent();
|
||||
return this.finish(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseWarnAndDebug(): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@debug') && !this.peek(TokenType.AtKeyword, '@warn')) {
|
||||
return null;
|
||||
}
|
||||
let node = this.createNode(nodes.NodeType.Debug);
|
||||
this.consumeToken(); // @debug or @warn
|
||||
node.addChild(this._parseExpr()); // optional
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseControlStatement(parseStatement: () => nodes.Node = this._parseRuleSetDeclaration.bind(this)): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword)) {
|
||||
return null;
|
||||
}
|
||||
return this._parseIfStatement(parseStatement) || this._parseForStatement(parseStatement)
|
||||
|| this._parseEachStatement(parseStatement) || this._parseWhileStatement(parseStatement);
|
||||
}
|
||||
|
||||
public _parseIfStatement(parseStatement: () => nodes.Node): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@if')) {
|
||||
return null;
|
||||
}
|
||||
return this._internalParseIfStatement(parseStatement);
|
||||
}
|
||||
|
||||
private _internalParseIfStatement(parseStatement: () => nodes.Node): nodes.IfStatement {
|
||||
let node = <nodes.IfStatement>this.create(nodes.IfStatement);
|
||||
this.consumeToken(); // @if or if
|
||||
if (!node.setExpression(this._parseBinaryExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected);
|
||||
}
|
||||
this._parseBody(node, parseStatement);
|
||||
if (this.accept(TokenType.AtKeyword, '@else')) {
|
||||
if (this.peek(TokenType.Ident, 'if')) {
|
||||
node.setElseClause(this._internalParseIfStatement(parseStatement));
|
||||
} else if (this.peek(TokenType.CurlyL)) {
|
||||
let elseNode = <nodes.BodyDeclaration>this.create(nodes.ElseStatement);
|
||||
this._parseBody(elseNode, parseStatement);
|
||||
node.setElseClause(elseNode);
|
||||
}
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseForStatement(parseStatement: () => nodes.Node): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@for')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.ForStatement>this.create(nodes.ForStatement);
|
||||
this.consumeToken(); // @for
|
||||
if (!node.setVariable(this._parseVariable())) {
|
||||
return this.finish(node, ParseError.VariableNameExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
if (!this.accept(TokenType.Ident, 'from')) {
|
||||
return this.finish(node, SCSSParseError.FromExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
if (!node.addChild(this._parseBinaryExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
if (!this.accept(TokenType.Ident, 'to') && !this.accept(TokenType.Ident, 'through')) {
|
||||
return this.finish(node, SCSSParseError.ThroughOrToExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
if (!node.addChild(this._parseBinaryExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
return this._parseBody(node, parseStatement);
|
||||
}
|
||||
|
||||
public _parseEachStatement(parseStatement: () => nodes.Node): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@each')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.EachStatement>this.create(nodes.EachStatement);
|
||||
this.consumeToken(); // @each
|
||||
if (!node.setVariable(this._parseVariable())) {
|
||||
return this.finish(node, ParseError.VariableNameExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
if (!this.accept(TokenType.Ident, 'in')) {
|
||||
return this.finish(node, SCSSParseError.InExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
if (!node.addChild(this._parseExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
return this._parseBody(node, parseStatement);
|
||||
}
|
||||
|
||||
public _parseWhileStatement(parseStatement: () => nodes.Node): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@while')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.WhileStatement>this.create(nodes.WhileStatement);
|
||||
this.consumeToken(); // @while
|
||||
if (!node.addChild(this._parseBinaryExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
return this._parseBody(node, parseStatement);
|
||||
}
|
||||
|
||||
public _parseFunctionBodyDeclaration(): nodes.Node {
|
||||
return this._parseVariableDeclaration() || this._parseReturnStatement()
|
||||
|| this._parseControlStatement(this._parseFunctionBodyDeclaration.bind(this));
|
||||
}
|
||||
|
||||
public _parseFunctionDeclaration(): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@function')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.FunctionDeclaration>this.create(nodes.FunctionDeclaration);
|
||||
this.consumeToken(); // @function
|
||||
|
||||
if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Function]))) {
|
||||
return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.ParenthesisL)) {
|
||||
return this.finish(node, ParseError.LeftParenthesisExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
if (node.getParameters().addChild(this._parseParameterDeclaration())) {
|
||||
while (this.accept(TokenType.Comma)) {
|
||||
if (!node.getParameters().addChild(this._parseParameterDeclaration())) {
|
||||
return this.finish(node, ParseError.VariableNameExpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
return this._parseBody(node, this._parseFunctionBodyDeclaration.bind(this));
|
||||
}
|
||||
|
||||
public _parseReturnStatement(): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@return')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = this.createNode(nodes.NodeType.ReturnStatement);
|
||||
this.consumeToken(); // @function
|
||||
|
||||
if (!node.addChild(this._parseExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected);
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMixinDeclaration(): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@mixin')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.MixinDeclaration>this.create(nodes.MixinDeclaration);
|
||||
this.consumeToken();
|
||||
|
||||
if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) {
|
||||
return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
if (this.accept(TokenType.ParenthesisL)) {
|
||||
if (node.getParameters().addChild(this._parseParameterDeclaration())) {
|
||||
while (this.accept(TokenType.Comma)) {
|
||||
if (!node.getParameters().addChild(this._parseParameterDeclaration())) {
|
||||
return this.finish(node, ParseError.VariableNameExpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
}
|
||||
|
||||
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
|
||||
}
|
||||
|
||||
public _parseParameterDeclaration(): nodes.Node {
|
||||
|
||||
let node = <nodes.FunctionParameter>this.create(nodes.FunctionParameter);
|
||||
|
||||
if (!node.setIdentifier(this._parseVariable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.accept(sassScanner.Ellipsis)) {
|
||||
// ok
|
||||
}
|
||||
|
||||
if (this.accept(TokenType.Colon)) {
|
||||
if (!node.setDefaultValue(this._parseExpr(true))) {
|
||||
return this.finish(node, ParseError.VariableValueExpected, [], [TokenType.Comma, TokenType.ParenthesisR]);
|
||||
}
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMixinContent(): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@content')) {
|
||||
return null;
|
||||
}
|
||||
let node = this.createNode(nodes.NodeType.MixinContent);
|
||||
this.consumeToken();
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
|
||||
public _parseMixinReference(): nodes.Node {
|
||||
if (!this.peek(TokenType.AtKeyword, '@include')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.MixinReference>this.create(nodes.MixinReference);
|
||||
this.consumeToken();
|
||||
|
||||
if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) {
|
||||
return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]);
|
||||
}
|
||||
|
||||
if (this.accept(TokenType.ParenthesisL)) {
|
||||
if (node.getArguments().addChild(this._parseFunctionArgument())) {
|
||||
while (this.accept(TokenType.Comma)) {
|
||||
if (!node.getArguments().addChild(this._parseFunctionArgument())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.peek(TokenType.CurlyL)) {
|
||||
let content = <nodes.BodyDeclaration>this.create(nodes.BodyDeclaration);
|
||||
this._parseBody(content, this._parseMixinReferenceBodyStatement.bind(this));
|
||||
node.setContent(content);
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMixinReferenceBodyStatement(): nodes.Node {
|
||||
return this._parseRuleSetDeclaration() || this._parseKeyframeSelector();
|
||||
}
|
||||
|
||||
public _parseFunctionArgument(): nodes.Node {
|
||||
// [variableName ':'] expression | variableName '...'
|
||||
let node = <nodes.FunctionArgument>this.create(nodes.FunctionArgument);
|
||||
|
||||
let pos = this.mark();
|
||||
let argument = this._parseVariable();
|
||||
if (argument) {
|
||||
if (!this.accept(TokenType.Colon)) {
|
||||
if (this.accept(sassScanner.Ellipsis)) { // optional
|
||||
node.setValue(argument);
|
||||
return this.finish(node);
|
||||
} else {
|
||||
this.restoreAtMark(pos);
|
||||
}
|
||||
} else {
|
||||
node.setIdentifier(argument);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.setValue(this._parseExpr(true))) {
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
115
extensions/css/server/src/parser/scssScanner.ts
Normal file
115
extensions/css/server/src/parser/scssScanner.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import {TokenType, Scanner, IToken} from './cssScanner';
|
||||
|
||||
const _FSL = '/'.charCodeAt(0);
|
||||
const _NWL = '\n'.charCodeAt(0);
|
||||
const _CAR = '\r'.charCodeAt(0);
|
||||
const _LFD = '\f'.charCodeAt(0);
|
||||
|
||||
const _DLR = '$'.charCodeAt(0);
|
||||
const _HSH = '#'.charCodeAt(0);
|
||||
const _CUL = '{'.charCodeAt(0);
|
||||
const _EQS = '='.charCodeAt(0);
|
||||
const _BNG = '!'.charCodeAt(0);
|
||||
const _LAN = '<'.charCodeAt(0);
|
||||
const _RAN = '>'.charCodeAt(0);
|
||||
const _DOT = '.'.charCodeAt(0);
|
||||
|
||||
let customTokenValue = TokenType.CustomToken;
|
||||
|
||||
export const VariableName = customTokenValue++;
|
||||
export const InterpolationFunction: TokenType = customTokenValue++;
|
||||
export const Default: TokenType = customTokenValue++;
|
||||
export const EqualsOperator: TokenType = customTokenValue++;
|
||||
export const NotEqualsOperator: TokenType = customTokenValue++;
|
||||
export const GreaterEqualsOperator: TokenType = customTokenValue++;
|
||||
export const SmallerEqualsOperator: TokenType = customTokenValue++;
|
||||
export const Ellipsis: TokenType = customTokenValue++;
|
||||
|
||||
export class SCSSScanner extends Scanner {
|
||||
|
||||
public scan(): IToken {
|
||||
|
||||
// processes all whitespaces and comments
|
||||
const triviaToken = this.trivia();
|
||||
if (triviaToken !== null) {
|
||||
return triviaToken;
|
||||
}
|
||||
|
||||
const offset = this.stream.pos();
|
||||
|
||||
// sass variable
|
||||
if (this.stream.advanceIfChar(_DLR)) {
|
||||
const content = ['$'];
|
||||
if (this.ident(content)) {
|
||||
return this.finishToken(offset, VariableName, content.join(''));
|
||||
} else {
|
||||
this.stream.goBackTo(offset);
|
||||
}
|
||||
}
|
||||
|
||||
// Sass: interpolation function #{..})
|
||||
if (this.stream.advanceIfChars([_HSH, _CUL])) {
|
||||
return this.finishToken(offset, InterpolationFunction);
|
||||
}
|
||||
|
||||
// operator ==
|
||||
if (this.stream.advanceIfChars([_EQS, _EQS])) {
|
||||
return this.finishToken(offset, EqualsOperator);
|
||||
}
|
||||
|
||||
// operator !=
|
||||
if (this.stream.advanceIfChars([_BNG, _EQS])) {
|
||||
return this.finishToken(offset, NotEqualsOperator);
|
||||
}
|
||||
|
||||
// operators <, <=
|
||||
if (this.stream.advanceIfChar(_LAN)) {
|
||||
if (this.stream.advanceIfChar(_EQS)) {
|
||||
return this.finishToken(offset, SmallerEqualsOperator);
|
||||
}
|
||||
return this.finishToken(offset, TokenType.Delim);
|
||||
}
|
||||
|
||||
// ooperators >, >=
|
||||
if (this.stream.advanceIfChar(_RAN)) {
|
||||
if (this.stream.advanceIfChar(_EQS)) {
|
||||
return this.finishToken(offset, GreaterEqualsOperator);
|
||||
}
|
||||
return this.finishToken(offset, TokenType.Delim);
|
||||
}
|
||||
|
||||
// ellipis
|
||||
if (this.stream.advanceIfChars([_DOT, _DOT, _DOT])) {
|
||||
return this.finishToken(offset, Ellipsis);
|
||||
}
|
||||
|
||||
return super.scan();
|
||||
}
|
||||
|
||||
protected comment(): boolean {
|
||||
if (super.comment()) {
|
||||
return true;
|
||||
}
|
||||
if (this.stream.advanceIfChars([_FSL, _FSL])) {
|
||||
this.stream.advanceWhileChar((ch: number) => {
|
||||
switch (ch) {
|
||||
case _NWL:
|
||||
case _CAR:
|
||||
case _LFD:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
extensions/css/server/src/services/scssCompletion.ts
Normal file
158
extensions/css/server/src/services/scssCompletion.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as languageFacts from './languageFacts';
|
||||
import {CSSCompletion} from './cssCompletion';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import {CompletionList, CompletionItemKind} from 'vscode-languageserver';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class SCSSCompletion extends CSSCompletion {
|
||||
|
||||
private static variableDefaults: { [key: string]: string; } = {
|
||||
'$red': '1',
|
||||
'$green': '2',
|
||||
'$blue': '3',
|
||||
'$alpha': '1.0',
|
||||
'$color': '$color',
|
||||
'$weight': '0.5',
|
||||
'$hue': '0',
|
||||
'$saturation': '0%',
|
||||
'$lightness': '0%',
|
||||
'$degrees': '0',
|
||||
'$amount': '0',
|
||||
'$string': '""',
|
||||
'$substring': '"s"',
|
||||
'$number': '0',
|
||||
'$limit': '1'
|
||||
};
|
||||
|
||||
private static colorProposals = [
|
||||
{ func: 'red($color)', desc: localize('scss.builtin.red', 'Gets the red component of a color.') },
|
||||
{ func: 'green($color)', desc: localize('scss.builtin.green', 'Gets the green component of a color.') },
|
||||
{ func: 'blue($color)', desc: localize('scss.builtin.blue', 'Gets the blue component of a color.') },
|
||||
{ func: 'mix($color, $color, [$weight])', desc: localize('scss.builtin.mix', 'Mixes two colors together.') },
|
||||
{ func: 'hue($color)', desc: localize('scss.builtin.hue', 'Gets the hue component of a color.') },
|
||||
{ func: 'saturation($color)', desc: localize('scss.builtin.saturation', 'Gets the saturation component of a color.') },
|
||||
{ func: 'lightness($color)', desc: localize('scss.builtin.lightness', 'Gets the lightness component of a color.') },
|
||||
{ func: 'adjust-hue($color, $degrees)', desc: localize('scss.builtin.adjust-hue', 'Changes the hue of a color.') },
|
||||
{ func: 'lighten($color, $amount)', desc: localize('scss.builtin.lighten', 'Makes a color lighter.') },
|
||||
{ func: 'darken($color, $amount)', desc: localize('scss.builtin.darken', 'Makes a color darker.') },
|
||||
{ func: 'saturate($color, $amount)', desc: localize('scss.builtin.saturate', 'Makes a color more saturated.') },
|
||||
{ func: 'desaturate($color, $amount)', desc: localize('scss.builtin.desaturate', 'Makes a color less saturated.') },
|
||||
{ func: 'grayscale($color)', desc: localize('scss.builtin.grayscale', 'Converts a color to grayscale.') },
|
||||
{ func: 'complement($color)', desc: localize('scss.builtin.complement', 'Returns the complement of a color.') },
|
||||
{ func: 'invert($color)', desc: localize('scss.builtin.invert', 'Returns the inverse of a color.') },
|
||||
{ func: 'alpha($color)', desc: localize('scss.builtin.alpha', 'Gets the opacity component of a color.') },
|
||||
{ func: 'opacity($color)', desc: 'Gets the alpha component (opacity) of a color.' },
|
||||
{ func: 'rgba($color, $alpha)', desc: localize('scss.builtin.rgba', 'Changes the alpha component for a color.') },
|
||||
{ func: 'opacify($color, $amount)', desc: localize('scss.builtin.opacify', 'Makes a color more opaque.') },
|
||||
{ func: 'fade-in($color, $amount)', desc: localize('scss.builtin.fade-in', 'Makes a color more opaque.') },
|
||||
{ func: 'transparentize($color, $amount) / fade-out($color, $amount)', desc: localize('scss.builtin.transparentize', 'Makes a color more transparent.') },
|
||||
{ func: 'adjust-color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha])', desc: localize('scss.builtin.adjust-color', 'Increases or decreases one or more components of a color.') },
|
||||
{ func: 'scale-color($color, [$red], [$green], [$blue], [$saturation], [$lightness], [$alpha])', desc: localize('scss.builtin.scale-color', 'Fluidly scales one or more properties of a color.') },
|
||||
{ func: 'change-color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha])', desc: localize('scss.builtin.change-color', 'Changes one or more properties of a color.') },
|
||||
{ func: 'ie-hex-str($color)', desc: localize('scss.builtin.ie-hex-str', 'Converts a color into the format understood by IE filters.') }
|
||||
];
|
||||
|
||||
private static selectorFuncs = [
|
||||
{ func: 'selector-nest($selectors…)', desc: localize('scss.builtin.selector-nest', 'Nests selector beneath one another like they would be nested in the stylesheet.') },
|
||||
{ func: 'selector-append($selectors…)', desc: localize('scss.builtin.selector-append', 'Appends selectors to one another without spaces in between.') },
|
||||
{ func: 'selector-extend($selector, $extendee, $extender)', desc: localize('scss.builtin.selector-extend', 'Extends $extendee with $extender within $selector.') },
|
||||
{ func: 'selector-replace($selector, $original, $replacement)', desc: localize('scss.builtin.selector-replace', 'Replaces $original with $replacement within $selector.') },
|
||||
{ func: 'selector-unify($selector1, $selector2)', desc: localize('scss.builtin.selector-unify', 'Unifies two selectors to produce a selector that matches elements matched by both.') },
|
||||
{ func: 'is-superselector($super, $sub)', desc: localize('scss.builtin.is-superselector', 'Returns whether $super matches all the elements $sub does, and possibly more.') },
|
||||
{ func: 'simple-selectors($selector)', desc: localize('scss.builtin.simple-selectors', 'Returns the simple selectors that comprise a compound selector.') },
|
||||
{ func: 'selector-parse($selector)', desc: localize('scss.builtin.selector-parse', 'Parses a selector into the format returned by &.') }
|
||||
];
|
||||
|
||||
private static builtInFuncs = [
|
||||
{ func: 'unquote($string)', desc: localize('scss.builtin.unquote', 'Removes quotes from a string.') },
|
||||
{ func: 'quote($string)', desc: localize('scss.builtin.quote', 'Adds quotes to a string.') },
|
||||
{ func: 'str-length($string)', desc: localize('scss.builtin.str-length', 'Returns the number of characters in a string.') },
|
||||
{ func: 'str-insert($string, $insert, $index)', desc: localize('scss.builtin.str-insert', 'Inserts $insert into $string at $index.') },
|
||||
{ func: 'str-index($string, $substring)', desc: localize('scss.builtin.str-index', 'Returns the index of the first occurance of $substring in $string.') },
|
||||
{ func: 'str-slice($string, $start-at, [$end-at])', desc: localize('scss.builtin.str-slice', 'Extracts a substring from $string.') },
|
||||
{ func: 'to-upper-case($string)', desc: localize('scss.builtin.to-upper-case', 'Converts a string to upper case.') },
|
||||
{ func: 'to-lower-case($string)', desc: localize('scss.builtin.to-lower-case', 'Converts a string to lower case.') },
|
||||
{ func: 'percentage($number)', desc: localize('scss.builtin.percentage', 'Converts a unitless number to a percentage.') },
|
||||
{ func: 'round($number)', desc: localize('scss.builtin.round', 'Rounds a number to the nearest whole number.') },
|
||||
{ func: 'ceil($number)', desc: localize('scss.builtin.ceil', 'Rounds a number up to the next whole number.') },
|
||||
{ func: 'floor($number)', desc: localize('scss.builtin.floor', 'Rounds a number down to the previous whole number.') },
|
||||
{ func: 'abs($number)', desc: localize('scss.builtin.abs', 'Returns the absolute value of a number.') },
|
||||
{ func: 'min($numbers)', desc: localize('scss.builtin.min', 'Finds the minimum of several numbers.') },
|
||||
{ func: 'max($numbers)', desc: localize('scss.builtin.max', 'Finds the maximum of several numbers.') },
|
||||
{ func: 'random([$limit])', desc: localize('scss.builtin.random', 'Returns a random number.') },
|
||||
{ func: 'length($list)', desc: localize('scss.builtin.length', 'Returns the length of a list.') },
|
||||
{ func: 'nth($list, $n)', desc: localize('scss.builtin.nth', 'Returns a specific item in a list.') },
|
||||
{ func: 'set-nth($list, $n, $value)', desc: localize('scss.builtin.set-nth', 'Replaces the nth item in a list.') },
|
||||
{ func: 'join($list1, $list2, [$separator])', desc: localize('scss.builtin.join', 'Joins together two lists into one.') },
|
||||
{ func: 'append($list1, $val, [$separator])', desc: localize('scss.builtin.append', 'Appends a single value onto the end of a list.') },
|
||||
{ func: 'zip($lists)', desc: localize('scss.builtin.zip', 'Combines several lists into a single multidimensional list.') },
|
||||
{ func: 'index($list, $value)', desc: localize('scss.builtin.index', 'Returns the position of a value within a list.') },
|
||||
{ func: 'list-separator(#list)', desc: localize('scss.builtin.list-separator', 'Returns the separator of a list.') },
|
||||
{ func: 'map-get($map, $key)', desc: localize('scss.builtin.map-get', 'Returns the value in a map associated with a given key.') },
|
||||
{ func: 'map-merge($map1, $map2)', desc: localize('scss.builtin.map-merge', 'Merges two maps together into a new map.') },
|
||||
{ func: 'map-remove($map, $keys)', desc: localize('scss.builtin.map-remove', 'Returns a new map with keys removed.') },
|
||||
{ func: 'map-keys($map)', desc: localize('scss.builtin.map-keys', 'Returns a list of all keys in a map.') },
|
||||
{ func: 'map-values($map)', desc: localize('scss.builtin.map-values', 'Returns a list of all values in a map.') },
|
||||
{ func: 'map-has-key($map, $key)', desc: localize('scss.builtin.map-has-key', 'Returns whether a map has a value associated with a given key.') },
|
||||
{ func: 'keywords($args)', desc: localize('scss.builtin.keywords', 'Returns the keywords passed to a function that takes variable arguments.') },
|
||||
{ func: 'feature-exists($feature)', desc: localize('scss.builtin.feature-exists', 'Returns whether a feature exists in the current Sass runtime.') },
|
||||
{ func: 'variable-exists($name)', desc: localize('scss.builtin.variable-exists', 'Returns whether a variable with the given name exists in the current scope.') },
|
||||
{ func: 'global-variable-exists($name)', desc: localize('scss.builtin.global-variable-exists', 'Returns whether a variable with the given name exists in the global scope.') },
|
||||
{ func: 'function-exists($name)', desc: localize('scss.builtin.function-exists', 'Returns whether a function with the given name exists.') },
|
||||
{ func: 'mixin-exists($name)', desc: localize('scss.builtin.mixin-exists', 'Returns whether a mixin with the given name exists.') },
|
||||
{ func: 'inspect($value)', desc: localize('scss.builtin.inspect', 'Returns the string representation of a value as it would be represented in Sass.') },
|
||||
{ func: 'type-of($value)', desc: localize('scss.builtin.type-of', 'Returns the type of a value.') },
|
||||
{ func: 'unit($number)', desc: localize('scss.builtin.unit', 'Returns the unit(s) associated with a number.') },
|
||||
{ func: 'unitless($number)', desc: localize('scss.builtin.unitless', 'Returns whether a number has units.') },
|
||||
{ func: 'comparable($number1, $number2)', desc: localize('scss.builtin.comparable', 'Returns whether two numbers can be added, subtracted, or compared.') },
|
||||
{ func: 'call($name, $args…)', desc: localize('scss.builtin.call', 'Dynamically calls a Sass function.') }
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super('$');
|
||||
}
|
||||
|
||||
private createFunctionProposals(proposals: {func: string; desc: string; }[], result: CompletionList): CompletionList {
|
||||
let replaceFunction = (match: string, p1: string) => p1 + ': {{' + (SCSSCompletion.variableDefaults[p1] || '') + '}}';
|
||||
proposals.forEach((p) => {
|
||||
result.items.push({
|
||||
label: p.func.substr(0, p.func.indexOf('(')),
|
||||
detail: p.func,
|
||||
documentation: p.desc,
|
||||
insertText: p.func.replace(/\[?(\$\w+)\]?/g, replaceFunction),
|
||||
kind: CompletionItemKind.Function
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public getCompletionsForSelector(ruleSet: nodes.RuleSet, result: CompletionList): CompletionList {
|
||||
this.createFunctionProposals(SCSSCompletion.selectorFuncs, result);
|
||||
return super.getCompletionsForSelector(ruleSet, result);
|
||||
}
|
||||
|
||||
public getTermProposals(result: CompletionList): CompletionList {
|
||||
this.createFunctionProposals(SCSSCompletion.builtInFuncs, result);
|
||||
return super.getTermProposals(result);
|
||||
}
|
||||
|
||||
protected getColorProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
|
||||
this.createFunctionProposals(SCSSCompletion.colorProposals, result);
|
||||
return super.getColorProposals(entry, result);
|
||||
}
|
||||
|
||||
public getCompletionsForDeclarationProperty(result: CompletionList): CompletionList {
|
||||
this.getCompletionsForSelector(null, result);
|
||||
return super.getCompletionsForDeclarationProperty(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import {CSSCompletion} from '../services/cssCompletion';
|
||||
import {CSSCodeActions} from '../services/cssCodeActions';
|
||||
import {CSSValidation} from '../services/cssValidation';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import {CSSCompletion} from '../../services/cssCompletion';
|
||||
import {CSSCodeActions} from '../../services/cssCodeActions';
|
||||
import {CSSValidation} from '../../services/cssValidation';
|
||||
|
||||
import {CompletionList, TextDocument, TextEdit, Position, Range, Command} from 'vscode-languageserver';
|
||||
import {applyEdits} from './textEditSupport';
|
||||
import {applyEdits} from '../textEditSupport';
|
||||
|
||||
suite('CSS - Code Actions', () => {
|
||||
let testCodeActions = function (value: string, tokenBefore: string): Thenable<{ commands: Command[]; document: TextDocument; }> {
|
||||
@@ -5,37 +5,37 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import {CSSCompletion} from '../services/cssCompletion';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import {CSSCompletion} from '../../services/cssCompletion';
|
||||
|
||||
import {CompletionList, TextDocument, TextEdit, Position, CompletionItemKind} from 'vscode-languageserver';
|
||||
import {applyEdits} from './textEditSupport';
|
||||
import {applyEdits} from '../textEditSupport';
|
||||
|
||||
export interface ItemDescription {
|
||||
label: string;
|
||||
documentation?: string;
|
||||
kind?: CompletionItemKind;
|
||||
resultText?: string;
|
||||
}
|
||||
|
||||
export let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document?: TextDocument) {
|
||||
let matches = completions.items.filter(completion => {
|
||||
return completion.label === expected.label;
|
||||
});
|
||||
assert.equal(matches.length, 1, expected.label + " should only existing once: Actual: " + completions.items.map(c => c.label).join(', '));
|
||||
if (expected.documentation) {
|
||||
assert.equal(matches[0].documentation, expected.documentation);
|
||||
}
|
||||
if (expected.kind) {
|
||||
assert.equal(matches[0].kind, expected.kind);
|
||||
}
|
||||
if (document && expected.resultText) {
|
||||
assert.equal(applyEdits(document, [matches[0].textEdit]), expected.resultText);
|
||||
}
|
||||
};
|
||||
|
||||
suite('CSS - Completion', () => {
|
||||
|
||||
interface ItemDescription {
|
||||
label: string;
|
||||
documentation?: string;
|
||||
kind?: CompletionItemKind;
|
||||
resultText?: string;
|
||||
}
|
||||
|
||||
let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document?: TextDocument) {
|
||||
let matches = completions.items.filter(completion => {
|
||||
return completion.label === expected.label;
|
||||
});
|
||||
assert.equal(matches.length, 1, expected.label + " should only existing once: Actual: " + completions.items.map(c => c.label).join(', '));
|
||||
if (expected.documentation) {
|
||||
assert.equal(matches[0].documentation, expected.documentation);
|
||||
}
|
||||
if (expected.kind) {
|
||||
assert.equal(matches[0].kind, expected.kind);
|
||||
}
|
||||
if (document && expected.resultText) {
|
||||
assert.equal(applyEdits(document, [matches[0].textEdit]), expected.resultText);
|
||||
}
|
||||
};
|
||||
|
||||
let testCompletionFor = function (value: string, stringBefore: string, expected: { count?: number, items?: ItemDescription[] }): Thenable<void> {
|
||||
let idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as languageFacts from '../services/languageFacts';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import * as languageFacts from '../../services/languageFacts';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {TextDocument} from 'vscode-languageserver';
|
||||
|
||||
export function assertColor(parser: Parser, text: string, selection: string, isColor: boolean): void {
|
||||
@@ -5,10 +5,10 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import {LintVisitor} from '../services/lint';
|
||||
import {Rule, Rules} from '../services/lintRules';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import {LintVisitor} from '../../services/lint';
|
||||
import {Rule, Rules} from '../../services/lintRules';
|
||||
import {TextDocument} from 'vscode-languageserver';
|
||||
|
||||
export function assertEntries(node: nodes.Node, rules: nodes.IRule[]): void {
|
||||
@@ -5,10 +5,10 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {Scope, GlobalScope, ScopeBuilder} from '../parser/cssSymbolScope';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import {CSSNavigation} from '../services/cssNavigation';
|
||||
import {Scope, GlobalScope, ScopeBuilder} from '../../parser/cssSymbolScope';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import {CSSNavigation} from '../../services/cssNavigation';
|
||||
|
||||
import {TextDocument, DocumentHighlightKind} from 'vscode-languageserver';
|
||||
|
||||
@@ -17,7 +17,7 @@ export function assertScopesAndSymbols(p: Parser, input: string, expected: strin
|
||||
assert.equal(scopeToString(global), expected);
|
||||
}
|
||||
|
||||
export function assertHighlights(p: Parser, input: string, marker: string, expectedMatches: number, expectedWrites: number): Thenable<void> {
|
||||
export function assertHighlights(p: Parser, input: string, marker: string, expectedMatches: number, expectedWrites: number, elementName?: string): Thenable<void> {
|
||||
let document = TextDocument.create('test://test/test.css', 'css', 0, input);
|
||||
|
||||
let stylesheet = p.parseStylesheet(document);
|
||||
@@ -27,7 +27,7 @@ export function assertHighlights(p: Parser, input: string, marker: string, expec
|
||||
let position = document.positionAt(index);
|
||||
|
||||
return new CSSNavigation().findDocumentHighlights(document, position, stylesheet).then(highlights => {
|
||||
assert.equal(highlights.length, expectedMatches);
|
||||
assert.equal(highlights.length, expectedMatches, input);
|
||||
|
||||
let nWrites = 0;
|
||||
for (let highlight of highlights) {
|
||||
@@ -36,7 +36,7 @@ export function assertHighlights(p: Parser, input: string, marker: string, expec
|
||||
}
|
||||
let range = highlight.range;
|
||||
let start = document.offsetAt(range.start), end = document.offsetAt(range.end);
|
||||
assert.equal(document.getText().substring(start, end), marker);
|
||||
assert.equal(document.getText().substring(start, end), elementName || marker);
|
||||
}
|
||||
assert.equal(nWrites, expectedWrites);
|
||||
});
|
||||
@@ -170,7 +170,7 @@ suite('CSS - Symbols', () => {
|
||||
assertScopesAndSymbols(p, '@font-face { font-family: "Bitstream Vera Serif Bold"; }', '[]');
|
||||
});
|
||||
|
||||
test('mark occurrences', function (testDone) {
|
||||
test('mark highlights', function (testDone) {
|
||||
let p = new Parser();
|
||||
Promise.all([
|
||||
assertHighlights(p, '@keyframes id {}; #main { animation: id 4s linear 0s infinite alternate; }', 'id', 2, 1),
|
||||
@@ -5,8 +5,8 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
|
||||
export class PrintingVisitor implements nodes.IVisitor {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import {TokenType} from '../parser/cssScanner';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import {ParseError} from '../parser/cssErrors';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import {TokenType} from '../../parser/cssScanner';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {ParseError} from '../../parser/cssErrors';
|
||||
|
||||
export function assertNode(text: string, parser: Parser, f: () => nodes.Node): nodes.Node {
|
||||
let node = parser.internalParse(text, f);
|
||||
@@ -5,7 +5,7 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {Scanner, TokenType} from '../parser/cssScanner';
|
||||
import {Scanner, TokenType} from '../../parser/cssScanner';
|
||||
|
||||
suite('CSS - Scanner', () => {
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {Parser} from '../parser/cssParser';
|
||||
import * as nodes from '../parser/cssNodes';
|
||||
import * as selectorPrinter from '../services/selectorPrinting';
|
||||
import {Parser} from '../../parser/cssParser';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import * as selectorPrinter from '../../services/selectorPrinting';
|
||||
import {TextDocument} from 'vscode-languageserver';
|
||||
|
||||
function elementToString(element: selectorPrinter.Element): string {
|
||||
336
extensions/css/server/src/test/scss/example.scss
Normal file
336
extensions/css/server/src/test/scss/example.scss
Normal file
@@ -0,0 +1,336 @@
|
||||
// snippets from the Sass documentation at http://sass-lang.com/
|
||||
|
||||
/* css stuff */
|
||||
/* charset */
|
||||
@charset "UTF-8";
|
||||
|
||||
/* nested rules */
|
||||
#main {
|
||||
width: 97%;
|
||||
p, div {
|
||||
font-size: 2em;
|
||||
a { font-weight: bold; }
|
||||
}
|
||||
pre { font-size: 3em; }
|
||||
}
|
||||
|
||||
/* parent selector (&) */
|
||||
#main {
|
||||
color: black;
|
||||
a {
|
||||
font-weight: bold;
|
||||
&:hover { color: red; }
|
||||
}
|
||||
}
|
||||
|
||||
/* nested properties */
|
||||
.funky {
|
||||
font: 2px/3px {
|
||||
family: fantasy;
|
||||
size: 30em;
|
||||
weight: bold;
|
||||
}
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* nesting conflicts */
|
||||
tr.default {
|
||||
foo: { // properties
|
||||
foo : 1;
|
||||
}
|
||||
foo: 1px; // rule
|
||||
foo.bar { // selector
|
||||
foo : 1;
|
||||
}
|
||||
foo:bar { // selector
|
||||
foo : 1;
|
||||
}
|
||||
foo: 1px; // rule
|
||||
}
|
||||
|
||||
/* extended comment syntax */
|
||||
/* This comment is
|
||||
* several lines long.
|
||||
* since it uses the CSS comment syntax,
|
||||
* it will appear in the CSS output. */
|
||||
body { color: black; }
|
||||
|
||||
// These comments are only one line long each.
|
||||
// They won't appear in the CSS output,
|
||||
// since they use the single-line comment syntax.
|
||||
a { color: green; }
|
||||
|
||||
/* variables */
|
||||
$width: 5em;
|
||||
$width: "Second width?" !default;
|
||||
#main {
|
||||
$localvar: 6em;
|
||||
width: $width;
|
||||
|
||||
$font-size: 12px;
|
||||
$line-height: 30px;
|
||||
font: #{$font-size}/#{$line-height};
|
||||
}
|
||||
$name: foo;
|
||||
$attr: border;
|
||||
p.#{$name} {
|
||||
#{$attr}-color: blue;
|
||||
}
|
||||
|
||||
/* variable declaration with whitespaces */
|
||||
// Set the color of your columns
|
||||
$grid-background-column-color : rgba(100, 100, 225, 0.25) !default;
|
||||
|
||||
/* operations*/
|
||||
p {
|
||||
width: (1em + 2em) * 3;
|
||||
color: #010203 + #040506;
|
||||
font-family: sans- + "serif";
|
||||
margin: 3px + 4px auto;
|
||||
content: "I ate #{5 + 10} pies!";
|
||||
color: hsl(0, 100%, 50%);
|
||||
color: hsl($hue: 0, $saturation: 100%, $lightness: 50%);
|
||||
}
|
||||
/* functions*/
|
||||
$grid-width: 40px;
|
||||
$gutter-width: 10px;
|
||||
@function grid-width($n) {
|
||||
@return $n * $grid-width + ($n - 1) * $gutter-width;
|
||||
}
|
||||
#sidebar { width: grid-width(5); }
|
||||
|
||||
/* @import */
|
||||
@import "foo.scss";
|
||||
$family: unquote("Droid+Sans");
|
||||
@import "rounded-corners", url("http://fonts.googleapis.com/css?family=#{$family}");
|
||||
#main {
|
||||
@import "example";
|
||||
}
|
||||
|
||||
/* @media */
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
@media screen and (orientation: landscape) {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
/* @extend */
|
||||
.error {
|
||||
border: 1px #f00;
|
||||
background-color: #fdd;
|
||||
}
|
||||
.seriousError {
|
||||
@extend .error;
|
||||
border-width: 3px;
|
||||
}
|
||||
#context a%extreme {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
}
|
||||
.notice {
|
||||
@extend %extreme !optional;
|
||||
}
|
||||
|
||||
/* @debug and @warn */
|
||||
@debug 10em + 12em;
|
||||
@mixin adjust-location($x, $y) {
|
||||
@if unitless($x) {
|
||||
@warn "Assuming #{$x} to be in pixels";
|
||||
$x: 1px * $x;
|
||||
}
|
||||
@if unitless($y) {
|
||||
@warn "Assuming #{$y} to be in pixels";
|
||||
$y: 1px * $y;
|
||||
}
|
||||
position: relative; left: $x; top: $y;
|
||||
}
|
||||
|
||||
/* control directives */
|
||||
|
||||
/* if statement */
|
||||
p {
|
||||
@if 1 + 1 == 2 { border: 1px solid; }
|
||||
@if 5 < 3 { border: 2px dotted; }
|
||||
@if null { border: 3px double; }
|
||||
}
|
||||
|
||||
/* if else statement */
|
||||
$type: monster;
|
||||
p {
|
||||
@if $type == ocean {
|
||||
color: blue;
|
||||
} @else {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
/* for statement */
|
||||
@for $i from 1 through 3 {
|
||||
.item-#{$i} { width: 2em * $i; }
|
||||
}
|
||||
|
||||
/* each statement */
|
||||
@each $animal in puma, sea-slug, egret, salamander {
|
||||
.#{$animal}-icon {
|
||||
background-image: url('/images/#{$animal}.png');
|
||||
}
|
||||
}
|
||||
|
||||
/* while statement */
|
||||
$i: 6;
|
||||
@while $i > 0 {
|
||||
.item-#{$i} { width: 2em * $i; }
|
||||
$i: $i - 2;
|
||||
}
|
||||
|
||||
/* function with controlstatements */
|
||||
@function foo($total, $a) {
|
||||
@for $i from 0 to $total {
|
||||
@if (unit($a) == "%") and ($i == ($total - 1)) {
|
||||
$z: 100%;
|
||||
@return '1';
|
||||
}
|
||||
}
|
||||
@return $grid;
|
||||
}
|
||||
|
||||
/* @mixin simple*/
|
||||
@mixin large-text {
|
||||
font: {
|
||||
family: Arial;
|
||||
size: 20px;
|
||||
weight: bold;
|
||||
}
|
||||
color: #ff0000;
|
||||
}
|
||||
.page-title {
|
||||
@include large-text;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* mixin with parameters */
|
||||
@mixin sexy-border($color, $width: 1in) {
|
||||
border: {
|
||||
color: $color;
|
||||
width: $width;
|
||||
style: dashed;
|
||||
}
|
||||
}
|
||||
p { @include sexy-border(blue); }
|
||||
|
||||
/* mixin with varargs */
|
||||
@mixin box-shadow($shadows...) {
|
||||
-moz-box-shadow: $shadows;
|
||||
-webkit-box-shadow: $shadows;
|
||||
box-shadow: $shadows;
|
||||
}
|
||||
.shadows {
|
||||
@include box-shadow(0px 4px 5px #666, 2px 6px 10px #999);
|
||||
}
|
||||
|
||||
/* include with varargs */
|
||||
@mixin colors($text, $background, $border) {
|
||||
color: $text;
|
||||
background-color: $background;
|
||||
border-color: $border;
|
||||
}
|
||||
$values: #ff0000, #00ff00, #0000ff;
|
||||
.primary {
|
||||
@include colors($values...);
|
||||
}
|
||||
|
||||
/* include with body */
|
||||
@mixin apply-to-ie6-only {
|
||||
* html {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
@include apply-to-ie6-only {
|
||||
#logo {
|
||||
background-image: url(/logo.gif);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* attributes */
|
||||
[rel="external"]::after {
|
||||
content: 's';
|
||||
}
|
||||
/*page */
|
||||
@page :left {
|
||||
margin-left: 4cm;
|
||||
margin-right: 3cm;
|
||||
}
|
||||
|
||||
/* missing semicolons */
|
||||
tr.default {
|
||||
foo.bar {
|
||||
$foo: 1px
|
||||
}
|
||||
foo: {
|
||||
foo : white
|
||||
}
|
||||
foo.bar1 {
|
||||
@extend tr.default
|
||||
}
|
||||
foo.bar2 {
|
||||
@import "compass"
|
||||
}
|
||||
bar: black
|
||||
}
|
||||
|
||||
/* rules without whitespace */
|
||||
legend {foo{a:s}margin-top:0;margin-bottom:#123;margin-top:s(1)}
|
||||
|
||||
/* extend with interpolation variable */
|
||||
@mixin error($a: false) {
|
||||
@extend .#{$a};
|
||||
@extend ##{$a};
|
||||
}
|
||||
#bar {a: 1px;}
|
||||
.bar {b: 1px;}
|
||||
foo {
|
||||
@include error('bar');
|
||||
}
|
||||
|
||||
/* css3: @font face */
|
||||
@font-face { font-family: Delicious; src: url('Delicious-Roman.otf'); }
|
||||
|
||||
/* rule names with variables */
|
||||
.orbit-#{$d}-prev {
|
||||
#{$d}-style: 0;
|
||||
foo-#{$d}: 1;
|
||||
#{$d}-bar-#{$d}: 2;
|
||||
foo-#{$d}-bar: 1;
|
||||
}
|
||||
|
||||
/* keyframes */
|
||||
@-webkit-keyframes NAME-YOUR-ANIMATION {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@-moz-keyframes NAME-YOUR-ANIMATION {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@-o-keyframes NAME-YOUR-ANIMATION {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@keyframes NAME-YOUR-ANIMATION {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* string escaping */
|
||||
[data-icon='test-1']:before {
|
||||
content:'\\';
|
||||
}
|
||||
/* a comment */
|
||||
$var1: '\'';
|
||||
$var2: "\"";
|
||||
/* another comment */
|
||||
|
||||
24
extensions/css/server/src/test/scss/languageFacts.test.ts
Normal file
24
extensions/css/server/src/test/scss/languageFacts.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import {SCSSParser} from '../../parser/scssParser';
|
||||
import {assertColor} from '../css/languageFacts.test';
|
||||
|
||||
suite('SCSS - Language facts', () => {
|
||||
|
||||
test('is color', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertColor(parser, '#main { color: foo(red) }', 'red', true);
|
||||
assertColor(parser, '#main { color: red() }', 'red', false);
|
||||
assertColor(parser, '#main { red { nested: 1px } }', 'red', false);
|
||||
assertColor(parser, '#main { @include red; }', 'red', false);
|
||||
assertColor(parser, '#main { @include foo($f: red); }', 'red', true);
|
||||
assertColor(parser, '@function red($p) { @return 1px; }', 'red', false);
|
||||
assertColor(parser, '@function foo($p) { @return red; }', 'red', true);
|
||||
assertColor(parser, '@function foo($r: red) { @return $r; }', 'red', true);
|
||||
});
|
||||
});
|
||||
|
||||
52
extensions/css/server/src/test/scss/lint.test.ts
Normal file
52
extensions/css/server/src/test/scss/lint.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import {Rule, Rules} from '../../services/lintRules';
|
||||
import {assertEntries} from '../css/lint.test';
|
||||
import {SCSSParser} from '../../parser/scssParser';
|
||||
|
||||
function assertFontFace(input: string, ...rules: Rule[]): void {
|
||||
let p = new SCSSParser();
|
||||
let node = p.internalParse(input, p._parseFontFace);
|
||||
|
||||
assertEntries(node, rules);
|
||||
}
|
||||
|
||||
function assertRuleSet(input: string, ...rules: Rule[]): void {
|
||||
let p = new SCSSParser();
|
||||
let node = p.internalParse(input, p._parseRuleset);
|
||||
assertEntries(node, rules);
|
||||
}
|
||||
|
||||
suite('SCSS - Lint', () => {
|
||||
|
||||
test('empty ruleset', function () {
|
||||
assertRuleSet('selector { color: red; nested {} }', Rules.EmptyRuleSet);
|
||||
});
|
||||
|
||||
test('font-face required properties', function () {
|
||||
assertFontFace('@font-face { }', Rules.RequiredPropertiesForFontFace);
|
||||
assertFontFace('@font-face { src: url(test.tff) }', Rules.RequiredPropertiesForFontFace);
|
||||
assertFontFace('@font-face { font-family: \'name\' }', Rules.RequiredPropertiesForFontFace);
|
||||
assertFontFace('@font-face { font-#{family}: foo }'); // no error, ignore all unknown properties
|
||||
assertFontFace('@font-face { font: {family: foo } }'); // no error, ignore all nested properties
|
||||
assertFontFace('@font-face { @if true { } }'); // no error, ignore all nested properties
|
||||
});
|
||||
|
||||
test('unknown properties', function () {
|
||||
assertRuleSet('selector { -ms-property: "rest is missing" }', Rules.UnknownProperty);
|
||||
assertRuleSet('selector { -moz-box-shadow: "rest is missing" }', Rules.UnknownProperty, Rules.IncludeStandardPropertyWhenUsingVendorPrefix);
|
||||
assertRuleSet('selector { box-shadow: none }'); // no error
|
||||
assertRuleSet('selector { -moz-#{box}-shadow: none }'); // no error if theres an interpolation
|
||||
assertRuleSet('selector { outer: { nested : blue }'); // no error for nested
|
||||
});
|
||||
|
||||
test('vendor specific prefixes', function () {
|
||||
assertRuleSet('selector { -moz-animation: none }', Rules.AllVendorPrefixes, Rules.IncludeStandardPropertyWhenUsingVendorPrefix);
|
||||
assertRuleSet('selector { -moz-transform: none; transform: none }', Rules.AllVendorPrefixes);
|
||||
assertRuleSet('selector { -moz-transform: none; transform: none; -o-transform: none; -webkit-transform: none; -ms-transform: none; }');
|
||||
});
|
||||
});
|
||||
335
extensions/css/server/src/test/scss/parser.test.ts
Normal file
335
extensions/css/server/src/test/scss/parser.test.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import {SCSSParser} from '../../parser/scssParser';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {ParseError} from '../../parser/cssErrors';
|
||||
import {SCSSParseError} from '../../parser/scssErrors';
|
||||
|
||||
import {assertNode, assertError} from '../css/parser.test';
|
||||
|
||||
suite('SCSS - Parser', () => {
|
||||
|
||||
test('Comments', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode(' a { b: /* comment */ c }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode(' a { b: /* comment \n * is several\n * lines long\n */ c }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode(' a { b: // single line comment\n c }', parser, parser._parseStylesheet.bind(parser));
|
||||
});
|
||||
|
||||
test('Variable', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('$color', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('$co42lor', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('$-co42lor', parser, parser._parseVariable.bind(parser));
|
||||
});
|
||||
|
||||
test('VariableDeclaration', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('$color: #F5F5F5', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('$color: 0', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('$color: 255', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('$color: 25.5', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('$color: 25px', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('$color: 25.5px !default', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('$primary-font: "wf_SegoeUI","Segoe UI","Segoe","Segoe WP"', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertError('$color: red !def', parser, parser._parseVariableDeclaration.bind(parser), ParseError.UnknownKeyword);
|
||||
assertError('$color : !default', parser, parser._parseVariableDeclaration.bind(parser), ParseError.VariableValueExpected);
|
||||
assertError('$color !default', parser, parser._parseVariableDeclaration.bind(parser), ParseError.ColonExpected);
|
||||
});
|
||||
|
||||
test('Expr', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('($let + 20)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('($let - 20)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('($let * 20)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('($let / 20)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 + $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 - $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 * $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 / $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 / 20 + $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 + 20 + $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 + 20 + 20 + $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 + 20 + 20 + 20 + $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 + 20 + $let + 20 + 20 + $let)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(20 + 20)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('($var1 + $var2)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(($let + 5) * 2)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(($let + (5 + 2)) * 2)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('($let + ((5 + 2) * 2))', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('$color', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('$color, $color', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('$color, 42%', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('$color, 42%, $color', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('$color - ($color + 10%)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('($base + $filler)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('(100% / 2 + $filler)', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('100% / 2 + $filler', parser, parser._parseExpr.bind(parser));
|
||||
assertNode('not ($v and $b) or $c', parser, parser._parseExpr.bind(parser));
|
||||
|
||||
assertError('(20 + 20', parser, parser._parseExpr.bind(parser), ParseError.RightParenthesisExpected);
|
||||
});
|
||||
|
||||
test('SassOperator', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('>=', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('>', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('<', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('<=', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('==', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('!=', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('and', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('+', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('-', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('*', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('/', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('%', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('not', parser, parser._parseUnaryOperator.bind(parser));
|
||||
});
|
||||
|
||||
test('Interpolation', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('#{red}', parser, parser._parseIdent.bind(parser));
|
||||
assertNode('#{$color}', parser, parser._parseIdent.bind(parser));
|
||||
assertNode('#{3 + 4}', parser, parser._parseIdent.bind(parser));
|
||||
assertNode('#{3 + #{3 + 4}}', parser, parser._parseIdent.bind(parser));
|
||||
assertNode('#{$d}-style: 0', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('foo-#{$d}: 1', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('#{$d}-bar-#{$d}: 2', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('foo-#{$d}-bar: 1', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('#{$d}-#{$d}: 2', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('&:nth-child(#{$query}+1) { clear: $opposite-direction; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertError('#{}', parser, parser._parseIdent.bind(parser), ParseError.ExpressionExpected);
|
||||
assertError('#{1 + 2', parser, parser._parseIdent.bind(parser), ParseError.RightCurlyExpected);
|
||||
});
|
||||
|
||||
test('Declaration', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('border: thin solid 1px', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: $color', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: blue', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: (20 / $let)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: (20 / 20 + $let)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: func($red)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: desaturate($red, 10%)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('dummy: desaturate(16, 10%)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('color: $base-color + #111', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('color: 100% / 2 + $ref', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('border: ($width * 2) solid black', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('property: $class', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('prop-erty: fnc($t, 10%)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('width: (1em + 2em) * 3', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('color: #010203 + #040506', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('font-family: sans- + "serif"', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('margin: 3px + 4px auto', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('color: hsl(0, 100%, 50%)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('color: hsl($hue: 0, $saturation: 100%, $lightness: 50%)', parser, parser._parseDeclaration.bind(parser));
|
||||
assertNode('foo: if($value == \'default\', flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser));
|
||||
|
||||
assertError('fo = 8', parser, parser._parseDeclaration.bind(parser), ParseError.ColonExpected);
|
||||
assertError('fo:', parser, parser._parseDeclaration.bind(parser), ParseError.PropertyValueExpected);
|
||||
assertError('color: hsl($hue: 0,', parser, parser._parseDeclaration.bind(parser), ParseError.ExpressionExpected);
|
||||
assertError('color: hsl($hue: 0', parser, parser._parseDeclaration.bind(parser), ParseError.RightParenthesisExpected);
|
||||
});
|
||||
|
||||
test('Stylesheet', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('$color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('$color: #F5F5F5; $color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('$color: #F5F5F5; $color: #F5F5F5; $color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('#main { width: 97%; p, div { a { font-weight: bold; } } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('a { &:hover { color: red; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('fo { font: 2px/3px { family: fantasy; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.foo { bar: { yoo: fantasy; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('selector { propsuffix: { nested: 1px; } rule: 1px; nested.selector { foo: 1; } nested:selector { foo: 2 }}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('legend {foo{a:s}margin-top:0;margin-bottom:#123;margin-top:s(1)}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin keyframe { @keyframes name { @content; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@include keyframe { 10% { top: 3px; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.class{&--sub-class-with-ampersand{color: red;}}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertError('fo { font: 2px/3px { family } }', parser, parser._parseStylesheet.bind(parser), ParseError.ColonExpected);
|
||||
});
|
||||
|
||||
test('@import', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@import "test.css"', parser, parser._parseImport.bind(parser));
|
||||
assertNode('@import url("test.css")', parser, parser._parseImport.bind(parser));
|
||||
assertNode('@import "test.css", "bar.css"', parser, parser._parseImport.bind(parser));
|
||||
assertNode('@import "test.css", "bar.css" screen, projection', parser, parser._parseImport.bind(parser));
|
||||
assertNode('foo { @import "test.css"; }', parser, parser._parseStylesheet.bind(parser));
|
||||
|
||||
assertError('@import "test.css" "bar.css"', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
|
||||
assertError('@import "test.css", screen', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected);
|
||||
assertError('@import', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected);
|
||||
});
|
||||
|
||||
test('@media', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@media #{$media} and ($feature: $value) {}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('foo { bar { @media screen and (orientation: landscape) {}} }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@media screen and (nth($query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser));
|
||||
});
|
||||
|
||||
test('@keyframe', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@keyframes name { @content; }', parser, parser._parseKeyframe.bind(parser));
|
||||
});
|
||||
|
||||
test('@extend', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('foo { @extend .error; border-width: 3px; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('a.important { @extend .notice !optional; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.hoverlink { @extend a:hover; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.seriousError { @extend .error; @extend .attention; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('#context a%extreme { color: blue; } .notice { @extend %extreme }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@media print { .error { } .seriousError { @extend .error; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin error($a: false) { @extend .#{$a}; @extend ##{$a}; }', parser, parser._parseStylesheet.bind(parser));
|
||||
|
||||
assertError('.hoverlink { @extend }', parser, parser._parseStylesheet.bind(parser), ParseError.SelectorExpected);
|
||||
assertError('.hoverlink { @extend %extreme !default }', parser, parser._parseStylesheet.bind(parser), ParseError.UnknownKeyword);
|
||||
});
|
||||
|
||||
test('@debug', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@debug test;', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('foo { @debug 1 + 4; nested { @warn 1 4; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@if $foo == 1 { @debug 1 + 4 }', parser, parser._parseStylesheet.bind(parser));
|
||||
});
|
||||
|
||||
test('@if', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@if 1 + 1 == 2 { border: 1px solid; }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('@if 5 < 3 { border: 2px dotted; }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('@if null { border: 3px double; }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('@if 1 <= $let { border: 3px; } @else { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('@if 1 >= (1 + $foo) { border: 3px; } @else if 1 + 1 == 2 { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('p { @if $i == 1 { x: 3px; } @else if $i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@if $i == 1 { p { x: 3px; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertError('@if { border: 1px solid; }', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected);
|
||||
assertError('@if 1 }', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.LeftCurlyExpected);
|
||||
});
|
||||
|
||||
test('@for', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@for $i from 1 to 5 { .item-#{$i} { width: 2em * $i; } }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('@for $k from 1 + $x through 5 + $x { }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertError('@for i from 0 to 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.VariableNameExpected);
|
||||
assertError('@for $i to 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), SCSSParseError.FromExpected);
|
||||
assertError('@for $i from 0 by 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), SCSSParseError.ThroughOrToExpected);
|
||||
assertError('@for $i from {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected);
|
||||
assertError('@for $i from 0 to {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected);
|
||||
});
|
||||
|
||||
test('@each', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@each $i in 1, 2, 3 { }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertNode('@each $i in 1 2 3 { }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertError('@each i in 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.VariableNameExpected);
|
||||
assertError('@each $i from 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), SCSSParseError.InExpected);
|
||||
assertError('@each $i in {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected);
|
||||
});
|
||||
|
||||
test('@while', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@while $i < 0 { .item-#{$i} { width: 2em * $i; } $i: $i - 2; }', parser, parser._parseRuleSetDeclaration.bind(parser));
|
||||
assertError('@while {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected);
|
||||
assertError('@while $i != 4', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.LeftCurlyExpected);
|
||||
assertError('@while ($i >= 4) {', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.RightCurlyExpected);
|
||||
});
|
||||
|
||||
test('@mixin', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@mixin large-text { font: { family: Arial; size: 20px; } color: #ff0000; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin sexy-border($color, $width: 1in) { color: black; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin box-shadow($shadows...) { -moz-box-shadow: $shadows; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin apply-to-ie6-only { * html { @content; } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin #{foo}($color){}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@mixin foo ($i:4) { size: $i; @include wee ($i - 1); }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertError('@mixin $1 {}', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
|
||||
assertError('@mixin foo() i {}', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected);
|
||||
assertError('@mixin foo(1) {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
|
||||
assertError('@mixin foo($color = 9) {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
|
||||
assertError('@mixin foo($color)', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected);
|
||||
assertError('@mixin foo($color){', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
|
||||
assertError('@mixin foo($color,){', parser, parser._parseStylesheet.bind(parser), ParseError.VariableNameExpected);
|
||||
});
|
||||
|
||||
test('@include', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('p { @include sexy-border(blue); }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.shadows { @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999); }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('$values: #ff0000, #00ff00, #0000ff; .primary { @include colors($values...); }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('p { @include apply-to-ie6-only { #logo { background-image: url(/logo.gif); } } }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertError('p { @include sexy-border blue', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
|
||||
assertError('p { @include sexy-border($values blue', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
|
||||
assertError('p { @include }', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
|
||||
assertError('p { @include foo($values }', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
|
||||
assertError('p { @include foo($values, }', parser, parser._parseStylesheet.bind(parser), ParseError.ExpressionExpected);
|
||||
|
||||
});
|
||||
|
||||
test('@function', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('@function grid-width($n) { @return $n * $grid-width + ($n - 1) * $gutter-width; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@function grid-width($n: 1, $e) { @return 0; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@function foo($total, $a) { @for $i from 0 to $total { } @return $grid; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@function foo() { @if (unit($a) == "%") and ($i == ($total - 1)) { @return 0; } @return 1; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@function is-even($int) { @if $int%2 == 0 { @return true; } @return false }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@function bar ($i) { @if $i > 0 { @return $i * bar($i - 1); } @return 1; }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertError('@function foo {} ', parser, parser._parseStylesheet.bind(parser), ParseError.LeftParenthesisExpected);
|
||||
assertError('@function {} ', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
|
||||
assertError('@function foo($a $b) {} ', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
|
||||
assertError('@function foo($a {} ', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected);
|
||||
assertError('@function foo($a...) { @return; }', parser, parser._parseStylesheet.bind(parser), ParseError.ExpressionExpected);
|
||||
assertError('@function foo($a,) {} ', parser, parser._parseStylesheet.bind(parser), ParseError.VariableNameExpected);
|
||||
assertError('@function foo($a:) {} ', parser, parser._parseStylesheet.bind(parser), ParseError.VariableValueExpected);
|
||||
|
||||
});
|
||||
|
||||
test('Ruleset', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('.selector { prop: erty $let 1px; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector:active { property:value; nested:hover {}}', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector {}', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { property: declaration }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { $variable: declaration }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { nested {}}', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { nested, a, b {}}', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { property: value; property: $value; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { property: value; @keyframes foo {} @-moz-keyframes foo {}}', parser, parser._parseRuleset.bind(parser));
|
||||
});
|
||||
|
||||
test('Nested Ruleset', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('.class1 { $let: 1; .class { $let: 2; three: $let; let: 3; } one: $let; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('.class1 { > .class2 { & > .class4 { rule1: v1; } } }', parser, parser._parseRuleset.bind(parser));
|
||||
});
|
||||
|
||||
test('Selector Interpolation', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('.#{$name} { }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('p.#{$name} { #{$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('sans-#{serif} { a-#{1 + 2}-color-#{$attr}: blue; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('##{f} .#{f} #{f}:#{f} { }', parser, parser._parseRuleset.bind(parser));
|
||||
});
|
||||
|
||||
test('Parent Selector', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('&:hover', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('&.float', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('&-bar', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('&&', parser, parser._parseSimpleSelector.bind(parser));
|
||||
});
|
||||
|
||||
test('Selector Placeholder', function () {
|
||||
let parser = new SCSSParser();
|
||||
assertNode('%hover', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('a%float', parser, parser._parseSimpleSelector.bind(parser));
|
||||
});
|
||||
});
|
||||
78
extensions/css/server/src/test/scss/scssCompletion.test.ts
Normal file
78
extensions/css/server/src/test/scss/scssCompletion.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
|
||||
import {SCSSParser} from '../../parser/scssParser';
|
||||
import {SCSSCompletion} from '../../services/scssCompletion';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {TextDocument, Position} from 'vscode-languageserver';
|
||||
import {assertCompletion, ItemDescription} from '../css/completion.test';
|
||||
|
||||
suite('SCSS - Completions', () => {
|
||||
|
||||
let testCompletionFor = function (value: string, stringBefore: string, expected: { count?: number, items?: ItemDescription[] }): Thenable<void> {
|
||||
let idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0;
|
||||
|
||||
let completionProvider = new SCSSCompletion();
|
||||
|
||||
let document = TextDocument.create('test://test/test.scss', 'scss', 0, value);
|
||||
let position = Position.create(0, idx);
|
||||
let jsonDoc = new SCSSParser().parseStylesheet(document);
|
||||
return completionProvider.doComplete(document, position, jsonDoc).then(list => {
|
||||
if (expected.count) {
|
||||
assert.equal(list.items, expected.count);
|
||||
}
|
||||
if (expected.items) {
|
||||
for (let item of expected.items) {
|
||||
assertCompletion(list, item, document);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
test('sylesheet', function (testDone): any {
|
||||
Promise.all([
|
||||
testCompletionFor('$i: 0; body { width: ', 'width: ', {
|
||||
items: [
|
||||
{ label: '$i' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('@for $i from 1 through 3 { .item-#{$i} { width: 2em * $i; } }', '.item-#{', {
|
||||
items: [
|
||||
{ label: '$i' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('.foo { background-color: d', 'background-color: d', {
|
||||
items: [
|
||||
{ label: 'darken' },
|
||||
{ label: 'desaturate' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('@function foo($x, $y) { @return $x + $y; } .foo { background-color: f', 'background-color: f', {
|
||||
items: [
|
||||
{ label: 'foo' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('.foo { di span { } ', 'di', {
|
||||
items: [
|
||||
{ label: 'display' },
|
||||
{ label: 'div' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('.foo { .', '{ .', {
|
||||
items: [
|
||||
{ label: '.foo' }
|
||||
]
|
||||
}),
|
||||
// issue #250
|
||||
testCompletionFor('.foo { display: block;', 'block;', {
|
||||
count: 0
|
||||
}),
|
||||
]).then(() => testDone(), (error) => testDone(error));
|
||||
|
||||
});
|
||||
});
|
||||
68
extensions/css/server/src/test/scss/scssNavigation.test.ts
Normal file
68
extensions/css/server/src/test/scss/scssNavigation.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import {SCSSParser} from '../../parser/scssParser';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {assertSymbolsInScope, assertScopesAndSymbols, assertHighlights} from '../css/navigation.test';
|
||||
|
||||
suite('SCSS - Symbols', () => {
|
||||
|
||||
test('symbols in scopes', function() {
|
||||
var p = new SCSSParser();
|
||||
assertSymbolsInScope(p, '$var: iable;', 0, { name:'$var', type:nodes.ReferenceType.Variable });
|
||||
assertSymbolsInScope(p, '$var: iable;', 11, { name:'$var', type:nodes.ReferenceType.Variable });
|
||||
assertSymbolsInScope(p, '$var: iable; .class { $color: blue; }', 11, { name:'$var', type:nodes.ReferenceType.Variable }, { name:'.class', type:nodes.ReferenceType.Rule });
|
||||
assertSymbolsInScope(p, '$var: iable; .class { $color: blue; }', 22, { name:'$color', type:nodes.ReferenceType.Variable });
|
||||
assertSymbolsInScope(p, '$var: iable; .class { $color: blue; }', 36, { name:'$color', type:nodes.ReferenceType.Variable });
|
||||
|
||||
assertSymbolsInScope(p, '@namespace "x"; @mixin mix() {}', 0, { name:'mix', type:nodes.ReferenceType.Mixin });
|
||||
assertSymbolsInScope(p, '@mixin mix { @mixin nested() {} }', 12, { name:'nested', type:nodes.ReferenceType.Mixin });
|
||||
assertSymbolsInScope(p, '@mixin mix () { @mixin nested() {} }', 13);
|
||||
});
|
||||
|
||||
test('scopes and symbols', function() {
|
||||
var p = new SCSSParser();
|
||||
assertScopesAndSymbols(p, '$var1: 1; $var2: 2; .foo { $var3: 3; }', '$var1,$var2,.foo,[$var3]');
|
||||
assertScopesAndSymbols(p, '@mixin mixin1 { $var0: 1} @mixin mixin2($var1) { $var3: 3 }', 'mixin1,mixin2,[$var0],[$var1,$var3]');
|
||||
assertScopesAndSymbols(p, 'a b { $var0: 1; c { d { } } }', '[$var0,c,[d,[]]]');
|
||||
assertScopesAndSymbols(p, '@function a($p1: 1, $p2: 2) { $v1: 3; @return $v1; }', 'a,[$p1,$p2,$v1]');
|
||||
assertScopesAndSymbols(p, '$var1: 3; @if $var1 == 2 { $var2: 1; } @else { $var2: 2; $var3: 2;} ', '$var1,[$var2],[$var2,$var3]');
|
||||
assertScopesAndSymbols(p, '@if $var1 == 2 { $var2: 1; } @else if $var1 == 2 { $var3: 2; } @else { $var3: 2; } ', '[$var2],[$var3],[$var3]');
|
||||
assertScopesAndSymbols(p, '$var1: 3; @while $var1 < 2 { #rule { a: b; } }', '$var1,[#rule,[]]');
|
||||
assertScopesAndSymbols(p, '$i:0; @each $name in f1, f2, f3 { $i:$i+1; }', '$i,[$name,$i]');
|
||||
assertScopesAndSymbols(p, '$i:0; @for $x from $i to 5 { }', '$i,[$x]');
|
||||
});
|
||||
|
||||
test('mark highlights', function(testDone) {
|
||||
var p = new SCSSParser();
|
||||
Promise.all([
|
||||
assertHighlights(p, '$var1: 1; $var2: /**/$var1;', '$var1', 2, 1),
|
||||
assertHighlights(p, '$var1: 1; p { $var2: /**/$var1; }', '/**/', 2, 1, '$var1'),
|
||||
assertHighlights(p, 'r1 { $var1: 1; p1: $var1;} r2,r3 { $var1: 1; p1: /**/$var1 + $var1;}', '/**/', 3, 1, '$var1'),
|
||||
assertHighlights(p, '.r1 { r1: 1em; } r2 { r1: 2em; @extend /**/.r1;}', '/**/', 2, 1, '.r1'),
|
||||
assertHighlights(p, '/**/%r1 { r1: 1em; } r2 { r1: 2em; @extend %r1;}', '/**/', 2, 1, '%r1'),
|
||||
assertHighlights(p, '@mixin r1 { r1: $p1; } r2 { r2: 2em; @include /**/r1; }', '/**/', 2, 1, 'r1'),
|
||||
assertHighlights(p, '@mixin r1($p1) { r1: $p1; } r2 { r2: 2em; @include /**/r1(2px); }', '/**/', 2, 1, 'r1'),
|
||||
assertHighlights(p, '$p1: 1; @mixin r1($p1: $p1) { r1: $p1; } r2 { r2: 2em; @include /**/r1; }', '/**/', 2, 1, 'r1'),
|
||||
assertHighlights(p, '/**/$p1: 1; @mixin r1($p1: $p1) { r1: $p1; }', '/**/', 2, 1, '$p1'),
|
||||
assertHighlights(p, '$p1 : 1; @mixin r1($p1) { r1: /**/$p1; }', '/**/', 2, 1, '$p1'),
|
||||
assertHighlights(p, '/**/$p1 : 1; @mixin r1($p1) { r1: $p1; }', '/**/', 1, 1, '$p1'),
|
||||
assertHighlights(p, '$p1 : 1; @mixin r1(/**/$p1) { r1: $p1; }', '/**/', 2, 1, '$p1'),
|
||||
assertHighlights(p, '$p1 : 1; @function r1($p1, $p2: /**/$p1) { @return $p1 + $p1 + $p2; }', '/**/', 2, 1, '$p1'),
|
||||
assertHighlights(p, '$p1 : 1; @function r1($p1, /**/$p2: $p1) { @return $p1 + $p2 + $p2; }', '/**/', 3, 1, '$p2'),
|
||||
assertHighlights(p, '@function r1($p1, $p2) { @return $p1 + $p2; } @function r2() { @return /**/r1(1, 2); }', '/**/', 2, 1, 'r1'),
|
||||
assertHighlights(p, '@function /**/r1($p1, $p2) { @return $p1 + $p2; } @function r2() { @return r1(1, 2); } p { x: r2(); }', '/**/', 2, 1, 'r1'),
|
||||
assertHighlights(p, '@function r1($p1, $p2) { @return $p1 + $p2; } @function r2() { @return r1(/**/$p1 : 1, $p2 : 2); } p { x: r2(); }', '/**/', 3, 1, '$p1'),
|
||||
|
||||
assertHighlights(p, '@mixin /*here*/foo { display: inline } foo { @include foo; }', '/*here*/', 2, 1, 'foo'),
|
||||
assertHighlights(p, '@mixin foo { display: inline } foo { @include /*here*/foo; }', '/*here*/', 2, 1, 'foo'),
|
||||
assertHighlights(p, '@mixin foo { display: inline } /*here*/foo { @include foo; }', '/*here*/', 1, 1, 'foo'),
|
||||
assertHighlights(p, '@function /*here*/foo($i) { @return $i*$i; } #foo { width: foo(2); }', '/*here*/', 2, 1, 'foo'),
|
||||
assertHighlights(p, '@function foo($i) { @return $i*$i; } #foo { width: /*here*/foo(2); }', '/*here*/', 2, 1, 'foo')
|
||||
]).then(() => testDone(), (error) => testDone(error));
|
||||
});
|
||||
|
||||
});
|
||||
34
extensions/css/server/src/test/scss/selectorPrinting.test.ts
Normal file
34
extensions/css/server/src/test/scss/selectorPrinting.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {SCSSParser} from '../../parser/scssParser';
|
||||
import {parseSelector} from '../css/selectorPrinting.test';
|
||||
|
||||
suite('SCSS - Selector Printing', () => {
|
||||
|
||||
test('nested selector', function () {
|
||||
let p = new SCSSParser();
|
||||
parseSelector(p, 'o1 { e1 { } }', 'e1', '{o1{…{e1}}}');
|
||||
parseSelector(p, 'o1 { e1.div { } }', 'e1', '{o1{…{e1[class=div]}}}');
|
||||
parseSelector(p, 'o1 o2 { e1 { } }', 'e1', '{o1{…{o2{…{e1}}}}}');
|
||||
parseSelector(p, 'o1, o2 { e1 { } }', 'e1', '{o1{…{e1}}}');
|
||||
parseSelector(p, 'o1 { @if $a { e1 { } } }', 'e1', '{o1{…{e1}}}');
|
||||
parseSelector(p, 'o1 { @mixin a { e1 { } } }', 'e1', '{e1}');
|
||||
parseSelector(p, 'o1 { @mixin a { e1 { } } }', 'e1', '{e1}');
|
||||
});
|
||||
|
||||
test('referencing selector', function () {
|
||||
let p = new SCSSParser();
|
||||
parseSelector(p, 'o1 { &:hover { }}', '&', '{o1[:hover=]}');
|
||||
parseSelector(p, 'o1 { &:hover & { }}', '&', '{o1[:hover=]{…{o1}}}');
|
||||
});
|
||||
|
||||
test('placeholders', function () {
|
||||
let p = new SCSSParser();
|
||||
parseSelector(p, '%o1 { e1 { } }', 'e1', '{%o1{…{e1}}}');
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
--ui tdd
|
||||
--useColors true
|
||||
./out/test
|
||||
./out/test/css
|
||||
./out/test/scss
|
||||
|
||||
Reference in New Issue
Block a user