mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 02:28:34 +01:00
Move LESS to extension
This commit is contained in:
@@ -17,6 +17,8 @@ import {CSSValidation} from './services/cssValidation';
|
||||
|
||||
import {SCSSParser} from './parser/scssParser';
|
||||
import {SCSSCompletion} from './services/scssCompletion';
|
||||
import {LESSParser} from './parser/lessParser';
|
||||
import {LESSCompletion} from './services/lessCompletion';
|
||||
|
||||
export interface LanguageService {
|
||||
configure(raw: LanguageSettings): void;
|
||||
@@ -37,12 +39,12 @@ export interface LanguageSettings {
|
||||
lint?: any;
|
||||
}
|
||||
|
||||
let cssParser = new Parser();
|
||||
let cssCompletion = new CSSCompletion();
|
||||
let cssHover = new CSSHover();
|
||||
let cssValidation = new CSSValidation();
|
||||
let cssNavigation = new CSSNavigation();
|
||||
let cssCodeActions = new CSSCodeActions();
|
||||
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 {
|
||||
@@ -68,4 +70,14 @@ export function getSCSSLanguageService() : LanguageService {
|
||||
languageService.parseStylesheet = scssParser.parseStylesheet.bind(scssParser);
|
||||
languageService.doComplete = scssCompletion.doComplete.bind(scssCompletion);
|
||||
return languageService;
|
||||
}
|
||||
|
||||
let lessParser = new LESSParser();
|
||||
let lessCompletion = new LESSCompletion();
|
||||
|
||||
export function getLESSLanguageService() : LanguageService {
|
||||
let languageService = getCSSLanguageService();
|
||||
languageService.parseStylesheet = lessParser.parseStylesheet.bind(lessParser);
|
||||
languageService.doComplete = lessCompletion.doComplete.bind(lessCompletion);
|
||||
return languageService;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TextDocuments, TextDocument, InitializeParams, InitializeResult, RequestType
|
||||
} from 'vscode-languageserver';
|
||||
|
||||
import {getCSSLanguageService, getSCSSLanguageService, LanguageSettings, LanguageService} from './cssLanguageService';
|
||||
import {getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService} from './cssLanguageService';
|
||||
import {Stylesheet} from './parser/cssNodes';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
@@ -57,7 +57,8 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
|
||||
let languageServices : { [id:string]: LanguageService} = {
|
||||
css: getCSSLanguageService(),
|
||||
scss: getSCSSLanguageService()
|
||||
scss: getSCSSLanguageService(),
|
||||
less: getLESSLanguageService()
|
||||
};
|
||||
|
||||
function getLanguageService(document: TextDocument) {
|
||||
|
||||
395
extensions/css/server/src/parser/lessParser.ts
Normal file
395
extensions/css/server/src/parser/lessParser.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 lessScanner from './lessScanner';
|
||||
import {TokenType} from './cssScanner';
|
||||
import * as cssParser from './cssParser';
|
||||
import * as nodes from './cssNodes';
|
||||
import {ParseError} from './cssErrors';
|
||||
|
||||
/// <summary>
|
||||
/// A parser for LESS
|
||||
/// http://lesscss.org/
|
||||
/// </summary>
|
||||
export class LESSParser extends cssParser.Parser {
|
||||
|
||||
public constructor() {
|
||||
super(new lessScanner.LESSScanner());
|
||||
}
|
||||
|
||||
public _parseStylesheetStatement(): nodes.Node {
|
||||
return this._tryParseMixinDeclaration() || super._parseStylesheetStatement() || this._parseVariableDeclaration();
|
||||
}
|
||||
|
||||
public _parseImport(): nodes.Node {
|
||||
let node = <nodes.Import>this.create(nodes.Import);
|
||||
if (!this.accept(TokenType.AtKeyword, '@import') && !this.accept(TokenType.AtKeyword, '@import-once') /* deprecated in less 1.4.1 */) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// less 1.4.1: @import (css) "lib"
|
||||
if (this.accept(TokenType.ParenthesisL)) {
|
||||
if (!this.accept(TokenType.Ident)) {
|
||||
return this.finish(node, ParseError.IdentifierExpected, [TokenType.SemiColon]);
|
||||
}
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.SemiColon]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) {
|
||||
return this.finish(node, ParseError.URIOrStringExpected, [TokenType.SemiColon]);
|
||||
}
|
||||
|
||||
node.setMedialist(this._parseMediaList());
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMediaQuery(resyncStopToken: TokenType[]): nodes.Node {
|
||||
let node = <nodes.MediaQuery>super._parseMediaQuery(resyncStopToken);
|
||||
if (!node) {
|
||||
let node = <nodes.MediaQuery>this.create(nodes.MediaQuery);
|
||||
if (node.addChild(this._parseVariable())) {
|
||||
return this.finish(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public _parseVariableDeclaration(panic: TokenType[] = []): nodes.VariableDeclaration {
|
||||
let node = <nodes.VariableDeclaration>this.create(nodes.VariableDeclaration);
|
||||
|
||||
let mark = this.mark();
|
||||
if (!node.setVariable(this._parseVariable())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.accept(TokenType.Colon, ':')) {
|
||||
node.colonPosition = this.prevToken.offset;
|
||||
if (!node.setValue(this._parseExpr())) {
|
||||
return <nodes.VariableDeclaration>this.finish(node, ParseError.VariableValueExpected, [], panic);
|
||||
}
|
||||
} else {
|
||||
this.restoreAtMark(mark);
|
||||
return null; // at keyword, but no ':', not a variable declaration but some at keyword
|
||||
}
|
||||
|
||||
if (this.peek(TokenType.SemiColon)) {
|
||||
node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist
|
||||
}
|
||||
|
||||
return <nodes.VariableDeclaration>this.finish(node);
|
||||
}
|
||||
|
||||
public _parseVariable(): nodes.Variable {
|
||||
let node = <nodes.Variable>this.create(nodes.Variable);
|
||||
let mark = this.mark();
|
||||
while (this.accept(TokenType.Delim, '@')) {
|
||||
if (this.hasWhitespace()) {
|
||||
this.restoreAtMark(mark);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!this.accept(TokenType.AtKeyword)) {
|
||||
this.restoreAtMark(mark);
|
||||
return null;
|
||||
}
|
||||
return <nodes.Variable>node;
|
||||
}
|
||||
|
||||
public _parseTerm(): nodes.Term {
|
||||
let term = super._parseTerm();
|
||||
if (term) { return term; }
|
||||
|
||||
term = <nodes.Term>this.create(nodes.Term);
|
||||
if (term.setExpression(this._parseVariable()) ||
|
||||
term.setExpression(this._parseEscaped())) {
|
||||
|
||||
return <nodes.Term>this.finish(term);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseEscaped(): nodes.Node {
|
||||
let node = this.createNode(nodes.NodeType.EscapedValue);
|
||||
if (this.accept(TokenType.EscapedJavaScript) ||
|
||||
this.accept(TokenType.BadEscapedJavaScript)) {
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
if (this.accept(TokenType.Delim, '~')) {
|
||||
return this.finish(node, this.accept(TokenType.String) ? null : ParseError.TermExpected);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseOperator(): nodes.Node {
|
||||
let node = this._parseGuardOperator();
|
||||
if (node) {
|
||||
return node;
|
||||
} else {
|
||||
return super._parseOperator();
|
||||
}
|
||||
}
|
||||
|
||||
public _parseGuardOperator(): nodes.Node {
|
||||
let node = this.createNode(nodes.NodeType.Operator);
|
||||
if (this.accept(TokenType.Delim, '>')) {
|
||||
this.accept(TokenType.Delim, '=');
|
||||
return node;
|
||||
} else if (this.accept(TokenType.Delim, '=')) {
|
||||
this.accept(TokenType.Delim, '<');
|
||||
return node;
|
||||
} else if (this.accept(TokenType.Delim, '<')) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public _parseRuleSetDeclaration(): nodes.Node {
|
||||
if (this.peek(TokenType.AtKeyword)) {
|
||||
return this._parseKeyframe()
|
||||
|| this._parseMedia()
|
||||
|| this._parseVariableDeclaration(); // Variable declarations
|
||||
}
|
||||
return this._tryParseMixinDeclaration()
|
||||
|| this._tryParseRuleset(true) // nested ruleset
|
||||
|| this._parseMixinReference() // less mixin reference
|
||||
|| this._parseExtend() // less extend declaration
|
||||
|| super._parseRuleSetDeclaration(); // try css ruleset declaration as the last option
|
||||
}
|
||||
|
||||
public _parseSimpleSelectorBody(): nodes.Node {
|
||||
return this._parseSelectorCombinator() || 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 _parseSelectorIdent(): nodes.Node {
|
||||
return this._parseIdent() || this._parseSelectorInterpolation();
|
||||
}
|
||||
|
||||
public _parseSelectorInterpolation(): nodes.Node {
|
||||
// Selector interpolation; old: ~"@{name}", new: @{name}
|
||||
let node = this.createNode(nodes.NodeType.SelectorInterpolation);
|
||||
if (this.accept(TokenType.Delim, '~')) {
|
||||
if (!this.hasWhitespace() && (this.accept(TokenType.String) || this.accept(TokenType.BadString))) {
|
||||
return this.finish(node);
|
||||
}
|
||||
return this.finish(node, ParseError.StringLiteralExpected);
|
||||
} else if (this.accept(TokenType.Delim, '@')) {
|
||||
if (this.hasWhitespace() || !this.accept(TokenType.CurlyL)) {
|
||||
return this.finish(node, ParseError.LeftCurlyExpected);
|
||||
}
|
||||
if (!node.addChild(this._parseIdent())) {
|
||||
return this.finish(node, ParseError.IdentifierExpected);
|
||||
}
|
||||
if (!this.accept(TokenType.CurlyR)) {
|
||||
return this.finish(node, ParseError.RightCurlyExpected);
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public _tryParseMixinDeclaration(): nodes.Node {
|
||||
if (!this.peek(TokenType.Delim, '.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let mark = this.mark();
|
||||
let node = <nodes.MixinDeclaration>this.create(nodes.MixinDeclaration);
|
||||
|
||||
if (!node.setIdentifier(this._parseMixinDeclarationIdentifier()) || !this.accept(TokenType.ParenthesisL)) {
|
||||
this.restoreAtMark(mark);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (node.getParameters().addChild(this._parseMixinParameter())) {
|
||||
while (this.accept(TokenType.Comma) || this.accept(TokenType.SemiColon)) {
|
||||
if (!node.getParameters().addChild(this._parseMixinParameter())) {
|
||||
return this.finish(node, ParseError.IdentifierExpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected);
|
||||
}
|
||||
node.setGuard(this._parseGuard());
|
||||
|
||||
if (!this.peek(TokenType.CurlyL)) {
|
||||
this.restoreAtMark(mark);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
|
||||
}
|
||||
|
||||
public _parseMixinDeclarationIdentifier(): nodes.Identifier {
|
||||
let identifier = <nodes.Identifier>this.create(nodes.Identifier); // identifier should contain dot
|
||||
this.consumeToken(); // .
|
||||
if (this.hasWhitespace() || !this.accept(TokenType.Ident)) {
|
||||
return null;
|
||||
}
|
||||
identifier.referenceTypes = [nodes.ReferenceType.Mixin];
|
||||
return this.finish(identifier);
|
||||
}
|
||||
|
||||
public _parseExtend(): nodes.Node {
|
||||
if (!this.peek(TokenType.Delim, '&')) {
|
||||
return null;
|
||||
}
|
||||
let mark = this.mark();
|
||||
|
||||
let node = <nodes.ExtendsReference>this.create(nodes.ExtendsReference);
|
||||
this.consumeToken(); // &
|
||||
if (this.hasWhitespace() || !this.accept(TokenType.Colon) || !this.accept(TokenType.Ident, 'extend')) {
|
||||
this.restoreAtMark(mark);
|
||||
return null;
|
||||
}
|
||||
if (!this.accept(TokenType.ParenthesisL)) {
|
||||
return this.finish(node, ParseError.LeftParenthesisExpected);
|
||||
}
|
||||
if (!node.setSelector(this._parseSimpleSelector())) {
|
||||
return this.finish(node, ParseError.SelectorExpected);
|
||||
}
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected);
|
||||
}
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMixinReference(): nodes.Node {
|
||||
if (!this.peek(TokenType.Delim, '.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let node = <nodes.MixinReference>this.create(nodes.MixinReference);
|
||||
|
||||
let identifier = <nodes.Identifier>this.create(nodes.Identifier);
|
||||
this.consumeToken(); // dot, part of the identifier
|
||||
if (this.hasWhitespace() || !this.accept(TokenType.Ident)) {
|
||||
return this.finish(node, ParseError.IdentifierExpected);
|
||||
}
|
||||
node.setIdentifier(this.finish(identifier));
|
||||
|
||||
if (!this.hasWhitespace() && this.accept(TokenType.ParenthesisL)) {
|
||||
if (node.getArguments().addChild(this._parseFunctionArgument())) {
|
||||
while (this.accept(TokenType.Comma) || this.accept(TokenType.SemiColon)) {
|
||||
if (!node.getArguments().addChild(this._parseExpr())) {
|
||||
return this.finish(node, ParseError.ExpressionExpected);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected);
|
||||
}
|
||||
identifier.referenceTypes = [nodes.ReferenceType.Mixin];
|
||||
} else {
|
||||
identifier.referenceTypes = [nodes.ReferenceType.Mixin, nodes.ReferenceType.Rule];
|
||||
}
|
||||
|
||||
node.addChild(this._parsePrio());
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseMixinParameter(): nodes.Node {
|
||||
|
||||
let node = <nodes.FunctionParameter>this.create(nodes.FunctionParameter);
|
||||
|
||||
// special rest variable: @rest...
|
||||
if (this.peek(TokenType.AtKeyword, '@rest')) {
|
||||
let restNode = this.create(nodes.Node);
|
||||
this.consumeToken();
|
||||
if (!this.accept(lessScanner.Ellipsis)) {
|
||||
return this.finish(node, ParseError.DotExpected, [], [TokenType.Comma, TokenType.ParenthesisR]);
|
||||
}
|
||||
node.setIdentifier(this.finish(restNode));
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
// special let args: ...
|
||||
if (this.peek(lessScanner.Ellipsis)) {
|
||||
let varargsNode = this.create(nodes.Node);
|
||||
this.consumeToken();
|
||||
node.setIdentifier(this.finish(varargsNode));
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
// default variable declaration: @param: 12 or @name
|
||||
if (node.setIdentifier(this._parseVariable())) {
|
||||
this.accept(TokenType.Colon);
|
||||
}
|
||||
node.setDefaultValue(this._parseExpr(true));
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseGuard(): nodes.LessGuard {
|
||||
|
||||
let node = <nodes.LessGuard>this.create(nodes.LessGuard);
|
||||
if (!this.accept(TokenType.Ident, 'when')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
node.isNegated = this.accept(TokenType.Ident, 'not');
|
||||
|
||||
if (!node.getConditions().addChild(this._parseGuardCondition())) {
|
||||
return <nodes.LessGuard>this.finish(node, ParseError.ConditionExpected);
|
||||
}
|
||||
while (this.accept(TokenType.Ident, 'and') || this.accept(TokenType.Comma, ',')) {
|
||||
if (!node.getConditions().addChild(this._parseGuardCondition())) {
|
||||
return <nodes.LessGuard>this.finish(node, ParseError.ConditionExpected);
|
||||
}
|
||||
}
|
||||
|
||||
return <nodes.LessGuard>this.finish(node);
|
||||
}
|
||||
|
||||
public _parseGuardCondition(): nodes.Node {
|
||||
let node = this.create(nodes.GuardCondition);
|
||||
if (!this.accept(TokenType.ParenthesisL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!node.addChild(this._parseExpr())) {
|
||||
// empty (?)
|
||||
}
|
||||
|
||||
if (!this.accept(TokenType.ParenthesisR)) {
|
||||
return this.finish(node, ParseError.RightParenthesisExpected);
|
||||
}
|
||||
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
public _parseFunctionIdentifier(): nodes.Identifier {
|
||||
if (this.peek(TokenType.Delim, '%')) {
|
||||
let node = <nodes.Identifier>this.create(nodes.Identifier);
|
||||
node.referenceTypes = [nodes.ReferenceType.Function];
|
||||
this.consumeToken();
|
||||
return this.finish(node);
|
||||
}
|
||||
|
||||
return super._parseFunctionIdentifier();
|
||||
}
|
||||
}
|
||||
73
extensions/css/server/src/parser/lessScanner.ts
Normal file
73
extensions/css/server/src/parser/lessScanner.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 scanner from './cssScanner';
|
||||
|
||||
const _FSL = '/'.charCodeAt(0);
|
||||
const _NWL = '\n'.charCodeAt(0);
|
||||
const _CAR = '\r'.charCodeAt(0);
|
||||
const _LFD = '\f'.charCodeAt(0);
|
||||
const _TIC = '`'.charCodeAt(0);
|
||||
const _DOT = '.'.charCodeAt(0);
|
||||
|
||||
let customTokenValue = scanner.TokenType.CustomToken;
|
||||
export const Ellipsis: scanner.TokenType = customTokenValue++;
|
||||
|
||||
export class LESSScanner extends scanner.Scanner {
|
||||
|
||||
public scan(): scanner.IToken {
|
||||
|
||||
let triviaToken = this.trivia();
|
||||
if (triviaToken !== null) {
|
||||
return triviaToken;
|
||||
}
|
||||
|
||||
let offset = this.stream.pos();
|
||||
|
||||
// LESS: escaped JavaScript code `let a = "dddd"`
|
||||
let tokenType = this.escapedJavaScript();
|
||||
if (tokenType !== null) {
|
||||
return this.finishToken(offset, tokenType);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private escapedJavaScript(): scanner.TokenType {
|
||||
let ch = this.stream.peekChar();
|
||||
if (ch === _TIC) {
|
||||
this.stream.advance(1);
|
||||
this.stream.advanceWhileChar((ch) => { return ch !== _TIC; });
|
||||
return this.stream.advanceIfChar(_TIC) ? scanner.TokenType.EscapedJavaScript : scanner.TokenType.BadEscapedJavaScript;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
353
extensions/css/server/src/services/lessCompletion.ts
Normal file
353
extensions/css/server/src/services/lessCompletion.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {CompletionList, CompletionItemKind} from 'vscode-languageserver';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class LESSCompletion extends CSSCompletion {
|
||||
|
||||
private static builtInProposals = [
|
||||
{
|
||||
'name': 'escape',
|
||||
'example': 'escape(@string);',
|
||||
'description': localize('less.builtin.escape', 'URL encodes a string')
|
||||
},
|
||||
{
|
||||
'name': 'e',
|
||||
'example': 'e(@string);',
|
||||
'description': localize('less.builtin.e', 'escape string content')
|
||||
},
|
||||
{
|
||||
'name': 'replace',
|
||||
'example': 'replace(@string, @pattern, @replacement[, @flags]);',
|
||||
'description': localize('less.builtin.replace', 'string replace')
|
||||
},
|
||||
{
|
||||
'name': 'unit',
|
||||
'example': 'unit(@dimension, [@unit: \'\']);',
|
||||
'description': localize('less.builtin.unit', 'remove or change the unit of a dimension')
|
||||
},
|
||||
{
|
||||
'name': 'color',
|
||||
'example': 'color(@string);',
|
||||
'description': localize('less.builtin.color', 'parses a string to a color')
|
||||
},
|
||||
{
|
||||
'name': 'convert',
|
||||
'example': 'convert(@value, unit);',
|
||||
'description': localize('less.builtin.convert', 'converts numbers from one type into another')
|
||||
},
|
||||
{
|
||||
'name': 'data-uri',
|
||||
'example': 'data-uri([mimetype,] url);',
|
||||
'description': localize('less.builtin.data-uri', 'inlines a resource and falls back to `url()`')
|
||||
},
|
||||
{
|
||||
'name': 'length',
|
||||
'example': 'length(@list);',
|
||||
'description': localize('less.builtin.length', 'returns the number of elements in a value list')
|
||||
},
|
||||
{
|
||||
'name': 'extract',
|
||||
'example': 'extract(@list, index);',
|
||||
'description': localize('less.builtin.extract', 'returns a value at the specified position in the list')
|
||||
},
|
||||
{
|
||||
'name': 'abs',
|
||||
'description': localize('less.builtin.abs', 'absolute value of a number'),
|
||||
'example': 'abs(number);'
|
||||
},
|
||||
{
|
||||
'name': 'acos',
|
||||
'description': localize('less.builtin.acos', 'arccosine - inverse of cosine function'),
|
||||
'example': 'acos(number);'
|
||||
},
|
||||
{
|
||||
'name': 'asin',
|
||||
'description': localize('less.builtin.asin', 'arcsine - inverse of sine function'),
|
||||
'example': 'asin(number);'
|
||||
},
|
||||
{
|
||||
'name': 'ceil',
|
||||
'example': 'ceil(@number);',
|
||||
'description': localize('less.builtin.ceil', 'rounds up to an integer')
|
||||
},
|
||||
{
|
||||
'name': 'cos',
|
||||
'description': localize('less.builtin.cos', 'cosine function'),
|
||||
'example': 'cos(number);'
|
||||
},
|
||||
{
|
||||
'name': 'floor',
|
||||
'description': localize('less.builtin.floor', 'rounds down to an integer'),
|
||||
'example': 'floor(@number);'
|
||||
},
|
||||
{
|
||||
'name': 'percentage',
|
||||
'description': localize('less.builtin.percentage', 'converts to a %, e.g. 0.5 > 50%'),
|
||||
'example': 'percentage(@number);'
|
||||
},
|
||||
{
|
||||
'name': 'round',
|
||||
'description': localize('less.builtin.round', 'rounds a number to a number of places'),
|
||||
'example': 'round(number, [places: 0]);'
|
||||
},
|
||||
{
|
||||
'name': 'sqrt',
|
||||
'description': localize('less.builtin.sqrt', 'calculates square root of a number'),
|
||||
'example': 'sqrt(number);'
|
||||
},
|
||||
{
|
||||
'name': 'sin',
|
||||
'description': localize('less.builtin.sin', 'sine function'),
|
||||
'example': 'sin(number);'
|
||||
},
|
||||
{
|
||||
'name': 'tan',
|
||||
'description': localize('less.builtin.tan', 'tangent function'),
|
||||
'example': 'tan(number);'
|
||||
},
|
||||
{
|
||||
'name': 'atan',
|
||||
'description': localize('less.builtin.atan', 'arctangent - inverse of tangent function'),
|
||||
'example': 'atan(number);'
|
||||
},
|
||||
{
|
||||
'name': 'pi',
|
||||
'description': localize('less.builtin.pi', 'returns pi'),
|
||||
'example': 'pi();'
|
||||
},
|
||||
{
|
||||
'name': 'pow',
|
||||
'description': localize('less.builtin.pow', 'first argument raised to the power of the second argument'),
|
||||
'example': 'pow(@base, @exponent);'
|
||||
},
|
||||
{
|
||||
'name': 'mod',
|
||||
'description': localize('less.builtin.mod', 'first argument modulus second argument'),
|
||||
'example': 'mod(number, number);'
|
||||
},
|
||||
{
|
||||
'name': 'min',
|
||||
'description': localize('less.builtin.min', 'returns the lowest of one or more values'),
|
||||
'example': 'min(@x, @y);'
|
||||
},
|
||||
{
|
||||
'name': 'max',
|
||||
'description': localize('less.builtin.max', 'returns the lowest of one or more values'),
|
||||
'example': 'max(@x, @y);'
|
||||
}
|
||||
];
|
||||
|
||||
private static colorProposals = [
|
||||
{
|
||||
'name': 'argb',
|
||||
'example': 'argb(@color);',
|
||||
'description': localize('less.builtin.argb', 'creates a #AARRGGBB')
|
||||
},
|
||||
{
|
||||
'name': 'hsl',
|
||||
'example': 'hsl(@hue, @saturation, @lightness);',
|
||||
'description': localize('less.builtin.hsl', 'creates a color')
|
||||
},
|
||||
{
|
||||
'name': 'hsla',
|
||||
'example': 'hsla(@hue, @saturation, @lightness, @alpha);',
|
||||
'description': localize('less.builtin.hsla', 'creates a color')
|
||||
},
|
||||
{
|
||||
'name': 'hsv',
|
||||
'example': 'hsv(@hue, @saturation, @value);',
|
||||
'description': localize('less.builtin.hsv', 'creates a color')
|
||||
},
|
||||
{
|
||||
'name': 'hsva',
|
||||
'example': 'hsva(@hue, @saturation, @value, @alpha);',
|
||||
'description': localize('less.builtin.hsva', 'creates a color')
|
||||
},
|
||||
{
|
||||
'name': 'hue',
|
||||
'example': 'hue(@color);',
|
||||
'description': localize('less.builtin.hue', 'returns the `hue` channel of `@color` in the HSL space')
|
||||
},
|
||||
{
|
||||
'name': 'saturation',
|
||||
'example': 'saturation(@color);',
|
||||
'description': localize('less.builtin.saturation', 'returns the `saturation` channel of `@color` in the HSL space')
|
||||
},
|
||||
{
|
||||
'name': 'lightness',
|
||||
'example': 'lightness(@color);',
|
||||
'description': localize('less.builtin.lightness', 'returns the `lightness` channel of `@color` in the HSL space')
|
||||
},
|
||||
{
|
||||
'name': 'hsvhue',
|
||||
'example': 'hsvhue(@color);',
|
||||
'description': localize('less.builtin.hsvhue', 'returns the `hue` channel of `@color` in the HSV space')
|
||||
},
|
||||
{
|
||||
'name': 'hsvsaturation',
|
||||
'example': 'hsvsaturation(@color);',
|
||||
'description': localize('less.builtin.hsvsaturation', 'returns the `saturation` channel of `@color` in the HSV space')
|
||||
},
|
||||
{
|
||||
'name': 'hsvvalue',
|
||||
'example': 'hsvvalue(@color);',
|
||||
'description': localize('less.builtin.hsvvalue', 'returns the `value` channel of `@color` in the HSV space')
|
||||
},
|
||||
{
|
||||
'name': 'red',
|
||||
'example': 'red(@color);',
|
||||
'description': localize('less.builtin.red', 'returns the `red` channel of `@color`')
|
||||
},
|
||||
{
|
||||
'name': 'green',
|
||||
'example': 'green(@color);',
|
||||
'description': localize('less.builtin.green', 'returns the `green` channel of `@color`')
|
||||
},
|
||||
{
|
||||
'name': 'blue',
|
||||
'example': 'blue(@color);',
|
||||
'description': localize('less.builtin.blue', 'returns the `blue` channel of `@color`')
|
||||
},
|
||||
{
|
||||
'name': 'alpha',
|
||||
'example': 'alpha(@color);',
|
||||
'description': localize('less.builtin.alpha', 'returns the `alpha` channel of `@color`')
|
||||
},
|
||||
{
|
||||
'name': 'luma',
|
||||
'example': 'luma(@color);',
|
||||
'description': localize('less.builtin.luma', 'returns the `luma` value (perceptual brightness) of `@color`')
|
||||
},
|
||||
{
|
||||
'name': 'saturate',
|
||||
'example': 'saturate(@color, 10%);',
|
||||
'description': localize('less.builtin.saturate', 'return `@color` 10% points more saturated')
|
||||
},
|
||||
{
|
||||
'name': 'desaturate',
|
||||
'example': 'desaturate(@color, 10%);',
|
||||
'description': localize('less.builtin.desaturate', 'return `@color` 10% points less saturated')
|
||||
},
|
||||
{
|
||||
'name': 'lighten',
|
||||
'example': 'lighten(@color, 10%);',
|
||||
'description': localize('less.builtin.lighten', 'return `@color` 10% points lighter')
|
||||
},
|
||||
{
|
||||
'name': 'darken',
|
||||
'example': 'darken(@color, 10%);',
|
||||
'description': localize('less.builtin.darken', 'return `@color` 10% points darker')
|
||||
},
|
||||
{
|
||||
'name': 'fadein',
|
||||
'example': 'fadein(@color, 10%);',
|
||||
'description': localize('less.builtin.fadein', 'return `@color` 10% points less transparent')
|
||||
},
|
||||
{
|
||||
'name': 'fadeout',
|
||||
'example': 'fadeout(@color, 10%);',
|
||||
'description': localize('less.builtin.fadeout', 'return `@color` 10% points more transparent')
|
||||
},
|
||||
{
|
||||
'name': 'fade',
|
||||
'example': 'fade(@color, 50%);',
|
||||
'description': localize('less.builtin.fade', 'return `@color` with 50% transparency')
|
||||
},
|
||||
{
|
||||
'name': 'spin',
|
||||
'example': 'spin(@color, 10);',
|
||||
'description': localize('less.builtin.spin', 'return `@color` with a 10 degree larger in hue')
|
||||
},
|
||||
{
|
||||
'name': 'mix',
|
||||
'example': 'mix(@color1, @color2, [@weight: 50%]);',
|
||||
'description': localize('less.builtin.mix', 'return a mix of `@color1` and `@color2`')
|
||||
},
|
||||
{
|
||||
'name': 'greyscale',
|
||||
'example': 'greyscale(@color);',
|
||||
'description': localize('less.builtin.greyscale', 'returns a grey, 100% desaturated color')
|
||||
},
|
||||
{
|
||||
'name': 'contrast',
|
||||
'example': 'contrast(@color1, [@darkcolor: black], [@lightcolor: white], [@threshold: 43%]);',
|
||||
'description': localize('less.builtin.contrast', 'return `@darkcolor` if `@color1 is> 43% luma` otherwise return `@lightcolor`, see notes')
|
||||
},
|
||||
{
|
||||
'name': 'multiply',
|
||||
'example': 'multiply(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'screen',
|
||||
'example': 'screen(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'overlay',
|
||||
'example': 'overlay(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'softlight',
|
||||
'example': 'softlight(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'hardlight',
|
||||
'example': 'hardlight(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'difference',
|
||||
'example': 'difference(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'exclusion',
|
||||
'example': 'exclusion(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'average',
|
||||
'example': 'average(@color1, @color2);'
|
||||
},
|
||||
{
|
||||
'name': 'negation',
|
||||
'example': 'negation(@color1, @color2);'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
constructor() {
|
||||
super('@');
|
||||
}
|
||||
|
||||
private createFunctionProposals(proposals: { name: string; example: string; description?: string; }[], result: CompletionList): CompletionList {
|
||||
proposals.forEach(p => {
|
||||
result.items.push({
|
||||
label: p.name,
|
||||
detail: p.example,
|
||||
documentation: p.description,
|
||||
insertText: p.name + '({{}})',
|
||||
kind: CompletionItemKind.Function
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public getTermProposals(result: CompletionList): CompletionList {
|
||||
this.createFunctionProposals(LESSCompletion.builtInProposals, result);
|
||||
return super.getTermProposals(result);
|
||||
}
|
||||
|
||||
protected getColorProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
|
||||
this.createFunctionProposals(LESSCompletion.colorProposals, result);
|
||||
return super.getColorProposals(entry, result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
73
extensions/css/server/src/test/less/lessCompletion.test.ts
Normal file
73
extensions/css/server/src/test/less/lessCompletion.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {LESSParser} from '../../parser/lessParser';
|
||||
import {LESSCompletion} from '../../services/lessCompletion';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {TextDocument, Position} from 'vscode-languageserver';
|
||||
import {assertCompletion, ItemDescription} from '../css/completion.test';
|
||||
|
||||
suite('LESS - 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 LESSCompletion();
|
||||
|
||||
let document = TextDocument.create('test://test/test.less', 'less', 0, value);
|
||||
let position = Position.create(0, idx);
|
||||
let jsonDoc = new LESSParser().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('body { ', '{ ', {
|
||||
items: [
|
||||
{ label: 'display' },
|
||||
{ label: 'background' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('body { ver', 'ver', {
|
||||
items: [
|
||||
{ label: 'vertical-align' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('body { word-break: ', ': ', {
|
||||
items: [
|
||||
{ label: 'keep-all' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('body { inner { vertical-align: }', ': ', {
|
||||
items: [
|
||||
{ label: 'bottom' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('@var1: 3; body { inner { vertical-align: }', 'align: ', {
|
||||
items: [
|
||||
{ label: '@var1' }
|
||||
]
|
||||
}),
|
||||
testCompletionFor('.foo { background-color: d', 'background-color: d', {
|
||||
items: [
|
||||
{ label: 'darken' },
|
||||
{ label: 'desaturate' }
|
||||
]
|
||||
})
|
||||
]).then(() => testDone(), (error) => testDone(error));
|
||||
});
|
||||
});
|
||||
55
extensions/css/server/src/test/less/lessNavigation.ts
Normal file
55
extensions/css/server/src/test/less/lessNavigation.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {LESSParser} from '../../parser/lessParser';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {assertScopeBuilding, assertSymbolsInScope, assertScopesAndSymbols, assertHighlights} from '../css/navigation.test';
|
||||
|
||||
suite('LESS - Symbols', () => {
|
||||
|
||||
test('scope building', function () {
|
||||
let p = new LESSParser();
|
||||
assertScopeBuilding(p, '@let: blue');
|
||||
assertScopeBuilding(p, '.class { .nested {} }', { offset: 7, length: 14 }, { offset: 17, length: 2 });
|
||||
});
|
||||
|
||||
test('symbols in scopes', function () {
|
||||
let p = new LESSParser();
|
||||
assertSymbolsInScope(p, '@let: iable;', 0, { name: '@let', type: nodes.ReferenceType.Variable });
|
||||
assertSymbolsInScope(p, '@let: iable;', 11, { name: '@let', type: nodes.ReferenceType.Variable });
|
||||
assertSymbolsInScope(p, '@let: iable; .class { @color: blue; }', 11, { name: '@let', type: nodes.ReferenceType.Variable }, { name: '.class', type: nodes.ReferenceType.Rule });
|
||||
assertSymbolsInScope(p, '@let: iable; .class { @color: blue; }', 21, { name: '@color', type: nodes.ReferenceType.Variable });
|
||||
assertSymbolsInScope(p, '@let: iable; .class { @color: blue; }', 36, { name: '@color', type: nodes.ReferenceType.Variable });
|
||||
|
||||
assertSymbolsInScope(p, '@namespace "x"; .mixin() {}', 0, { name: '.mixin', type: nodes.ReferenceType.Mixin });
|
||||
assertSymbolsInScope(p, '.mixin() { .nested() {} }', 10, { name: '.nested', type: nodes.ReferenceType.Mixin });
|
||||
assertSymbolsInScope(p, '.mixin() { .nested() {} }', 11);
|
||||
|
||||
assertSymbolsInScope(p, '@keyframes animation {};', 0, { name: 'animation', type: nodes.ReferenceType.Keyframe });
|
||||
});
|
||||
|
||||
test('scopes and symbols', function () {
|
||||
let p = new LESSParser();
|
||||
assertScopesAndSymbols(p, '@var1: 1; @var2: 2; .foo { @var3: 3; }', '@var1,@var2,.foo,[@var3]');
|
||||
assertScopesAndSymbols(p, '.mixin1 { @var0: 1} .mixin2(@var1) { @var3: 3 }', '.mixin1,.mixin2,[@var0],[@var1,@var3]');
|
||||
assertScopesAndSymbols(p, 'a b { @var0: 1; c { d { } } }', '[@var0,c,[d,[]]]');
|
||||
});
|
||||
|
||||
test('mark highlights', function (testDone) {
|
||||
let p = new LESSParser();
|
||||
Promise.all([
|
||||
assertHighlights(p, '@var1: 1; @var2: /**/@var1;', '/**/', 2, 1, '@var1'),
|
||||
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; /**/.r1;}', '/**/', 2, 1, '.r1'),
|
||||
assertHighlights(p, '.r1(@p1) { r1: @p1; } r2 { r1: 2em; /**/.r1(2px); }', '/**/', 2, 1, '.r1'),
|
||||
assertHighlights(p, '/**/.r1(@p1) { r1: @p1; } r2 { r1: 2em; .r1(2px); }', '/**/', 2, 1, '.r1'),
|
||||
assertHighlights(p, '@p1 : 1; .r1(@p1) { r1: /**/@p1; }', '/**/', 2, 1, '@p1'),
|
||||
assertHighlights(p, '/**/@p1 : 1; .r1(@p1) { r1: @p1; }', '/**/', 1, 1, '@p1'),
|
||||
assertHighlights(p, '@p1 : 1; .r1(/**/@p1) { r1: @p1; }', '/**/', 2, 1, '@p1'),
|
||||
]).then(() => testDone(), (error) => testDone(error));
|
||||
});
|
||||
});
|
||||
24
extensions/css/server/src/test/less/nodes.test.ts
Normal file
24
extensions/css/server/src/test/less/nodes.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 {assertNodes} from '../css/nodes.test';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {LESSParser} from '../../parser/lessParser';
|
||||
|
||||
suite('LESS - Nodes', () => {
|
||||
|
||||
function ruleset(input: string): nodes.RuleSet {
|
||||
let parser = new LESSParser();
|
||||
let node = parser.internalParse(input, parser._parseRuleset);
|
||||
return node;
|
||||
}
|
||||
|
||||
test('RuleSet', function () {
|
||||
assertNodes(ruleset, 'selector { prop: value }', 'ruleset,...,selector,...,declaration,...,property,...,expression');
|
||||
assertNodes(ruleset, 'selector { prop; }', 'ruleset,...,selector,...,selector');
|
||||
assertNodes(ruleset, 'selector { prop {} }', 'ruleset,...,ruleset');
|
||||
});
|
||||
});
|
||||
218
extensions/css/server/src/test/less/parser.test.ts
Normal file
218
extensions/css/server/src/test/less/parser.test.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {Parser} from '../../parser/cssParser';
|
||||
import {TokenType} from '../../parser/cssScanner';
|
||||
import * as nodes from '../../parser/cssNodes';
|
||||
import {ParseError} from '../../parser/cssErrors';
|
||||
import {LESSParser} from '../../parser/lessParser';
|
||||
|
||||
import {assertNode, assertNoNode, assertError} from '../css/parser.test';
|
||||
|
||||
suite('LESS - Parser', () => {
|
||||
|
||||
test('Variable', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('@color', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('@co42lor', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('@-co42lor', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('@@foo', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('@@@foo', parser, parser._parseVariable.bind(parser));
|
||||
assertNode('@12ooo', parser, parser._parseVariable.bind(parser));
|
||||
assertNoNode('@ @foo', parser, parser._parseFunction.bind(parser));
|
||||
assertNoNode('@-@foo', parser, parser._parseFunction.bind(parser));
|
||||
});
|
||||
|
||||
test('Media', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('@media @phone {}', parser, parser._parseMedia.bind(parser));
|
||||
});
|
||||
|
||||
test('VariableDeclaration', function() {
|
||||
let parser = new LESSParser();
|
||||
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', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('@primary-font: "wf_SegoeUI","Segoe UI","Segoe","Segoe WP"', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
assertNode('@greeting: `"hello".toUpperCase() + "!";`', parser, parser._parseVariableDeclaration.bind(parser));
|
||||
});
|
||||
|
||||
test('MixinDeclaration', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('.color (@color: 25.5px) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.color(@color: 25.5px) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.color(@color) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.color(@color; @border) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.color() { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.color( ) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.mixin (@a) when (@a > 10), (@a < -10) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.mixin (@a) when (isnumber(@a)) and (@a > 0) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.mixin (@b) when not (@b >= 0) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.mixin (@b) when not (@b > 0) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.mixin (@a, @rest...) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
assertNode('.mixin (@a) when (lightness(@a) >= 50%) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
|
||||
|
||||
});
|
||||
|
||||
test('MixinReference', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('.box-shadow(0 0 5px, 30%)', parser, parser._parseMixinReference.bind(parser));
|
||||
assertNode('.box-shadow', parser, parser._parseMixinReference.bind(parser));
|
||||
assertNode('.mixin(10) !important', parser, parser._parseMixinReference.bind(parser));
|
||||
});
|
||||
|
||||
test('MixinParameter', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('@_', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('@let: value', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('@let', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('@rest...', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('...', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('value', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('"string"', parser, parser._parseMixinParameter.bind(parser));
|
||||
assertNode('50%', parser, parser._parseMixinParameter.bind(parser));
|
||||
});
|
||||
|
||||
test('Parser - function', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('%()', parser, parser._parseFunction.bind(parser));
|
||||
assertNoNode('% ()', parser, parser._parseFunction.bind(parser));
|
||||
|
||||
});
|
||||
|
||||
test('Expr', function() {
|
||||
let parser = new LESSParser();
|
||||
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));
|
||||
});
|
||||
|
||||
test('LessOperator', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('>=', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('>', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('<', parser, parser._parseOperator.bind(parser));
|
||||
assertNode('=<', parser, parser._parseOperator.bind(parser));
|
||||
});
|
||||
|
||||
test('Extend', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('nav { &:extend(.inline); }', parser, parser._parseRuleset.bind(parser));
|
||||
});
|
||||
|
||||
test('Declaration', function() {
|
||||
let parser = new LESSParser();
|
||||
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));
|
||||
});
|
||||
|
||||
test('Stylesheet', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('.color (@radius: 5px){ -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.color (@radius: 5px){ -border-radius: @radius }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.color (@radius: 5px){ -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
|
||||
|
||||
assertNode('.mixin (@a, @rest...) {}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.mixin (@a) when (lightness(@a) >= 50%) { background-color: black;}', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('.some-mixin { font-weight:bold; } h1 { .some-mixin; font-size:40px; }', parser, parser._parseStylesheet.bind(parser));
|
||||
|
||||
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('@color: #F5F5F5; .color (@radius: 5px) { -border-radius: #F5F5F5 } @color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@import-once "lib";', parser, parser._parseStylesheet.bind(parser));
|
||||
assertNode('@import-once (css) "hello";', parser, parser._parseStylesheet.bind(parser));
|
||||
assertError('@import-once () "hello";', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
|
||||
assertError('@import-once (less);', parser, parser._parseStylesheet.bind(parser), ParseError.URIOrStringExpected);
|
||||
});
|
||||
|
||||
test('Ruleset', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('.selector { prop: erty @let 1px; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { .mixin; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { .mixin(1px); .mixin(blue, 1px, \'farboo\') }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('selector { .mixin(blue; 1px;\'farboo\') }', 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('term', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('%(\'repetitions: %S file: %S\', 1 + 2, "directory/file.less")', parser, parser._parseTerm.bind(parser));
|
||||
assertNode('~"ms:alwaysHasItsOwnSyntax.For.Stuff()"', parser, parser._parseTerm.bind(parser)); // less syntax
|
||||
});
|
||||
|
||||
test('Nested Ruleset', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('.class1 { @let: 1; .class { @let: 2; three: @let; let: 3; } one: @let; }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('.class1 { @let: 1; > .class2 { display: none; } }', parser, parser._parseRuleset.bind(parser));
|
||||
});
|
||||
|
||||
test('Selector Interpolation', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('.@{name} { }', parser, parser._parseRuleset.bind(parser));
|
||||
assertNode('~"@{name}" { }', parser, parser._parseRuleset.bind(parser));
|
||||
assertError('~{ }', parser, parser._parseStylesheet.bind(parser), ParseError.StringLiteralExpected);
|
||||
assertError('@', parser, parser._parseSelectorInterpolation.bind(parser), ParseError.LeftCurlyExpected);
|
||||
assertError('@{', parser, parser._parseSelectorInterpolation.bind(parser), ParseError.IdentifierExpected);
|
||||
assertError('@{dd', parser, parser._parseSelectorInterpolation.bind(parser), ParseError.RightCurlyExpected);
|
||||
});
|
||||
|
||||
test('Selector Combinator', function() {
|
||||
let parser = new LESSParser();
|
||||
assertNode('&:hover', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('&.float', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('&-foo', parser, parser._parseSimpleSelector.bind(parser));
|
||||
assertNode('&--&', parser, parser._parseSimpleSelector.bind(parser));
|
||||
});
|
||||
});
|
||||
39
extensions/css/server/src/test/less/scanner.test.ts
Normal file
39
extensions/css/server/src/test/less/scanner.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 {Scanner, TokenType} from '../../parser/cssScanner';
|
||||
import {LESSScanner} from '../../parser/lessScanner';
|
||||
|
||||
function assertSingleToken(source: string, len: number, offset: number, text: string, type: TokenType): void {
|
||||
let scan = new LESSScanner();
|
||||
scan.setSource(source);
|
||||
let token = scan.scan();
|
||||
assert.equal(token.len, len);
|
||||
assert.equal(token.offset, offset);
|
||||
assert.equal(token.text, text);
|
||||
assert.equal(token.type, type);
|
||||
}
|
||||
|
||||
suite('LESS - Scanner', () => {
|
||||
|
||||
test('Test Escaped JavaScript', function () {
|
||||
assertSingleToken('`', 1, 0, '`', TokenType.BadEscapedJavaScript);
|
||||
assertSingleToken('`a', 2, 0, '`a', TokenType.BadEscapedJavaScript);
|
||||
assertSingleToken('`let a = "ssss"`', 16, 0, '`let a = "ssss"`', TokenType.EscapedJavaScript);
|
||||
assertSingleToken('`let a = "ss\ns"`', 16, 0, '`let a = "ss\ns"`', TokenType.EscapedJavaScript);
|
||||
});
|
||||
|
||||
// less deactivated comments
|
||||
test('Test Token SingleLineComment', function () {
|
||||
assertSingleToken('//', 0, 2, '', TokenType.EOF);
|
||||
assertSingleToken('//this is a comment test', 0, 24, '', TokenType.EOF);
|
||||
assertSingleToken('// this is a comment test', 0, 25, '', TokenType.EOF);
|
||||
assertSingleToken('// this is a\na', 1, 13, 'a', TokenType.Ident);
|
||||
assertSingleToken('// this is a\n// more\n \n/* comment */a', 1, 38, 'a', TokenType.Ident);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user