提交 2b247ccb 编写于 作者: M Martin Aeschlimann

[css] move to extension

上级 966cb81e
......@@ -71,6 +71,7 @@ var copyrightFilter = [
'!**/*.bat',
'!**/*.cmd',
'!resources/win32/bin/code.js',
'!**/*.xml',
'!**/*.sh',
'!**/*.txt',
'!**/*.xpm',
......
......@@ -13,7 +13,8 @@ const extensions = [
'configuration-editing',
'typescript',
'php',
'javascript'
'javascript',
'css'
];
extensions.forEach(extension => {
......
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}"
],
"stopOnEntry": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/client/out",
"preLaunchTask": "npm"
},
{
"name": "Launch Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/client/out/test" ],
"stopOnEntry": false,
"sourceMaps": true,
"outDir": "${workspaceRoot}/client/out/test",
"preLaunchTask": "npm"
}
]
}
\ No newline at end of file
// Available variables which can be used inside of strings.
// ${workspaceRoot}: the root folder of the team
// ${file}: the current opened file
// ${fileBasename}: the current opened file's basename
// ${fileDirname}: the current opened file's dirname
// ${fileExtname}: the current opened file's extension
// ${cwd}: the current working directory of the spawned process
// A task runner that calls a custom npm script that compiles the extension.
{
"version": "0.1.0",
// we want to run npm
"command": "npm",
// the command is a shell script
"isShellCommand": true,
// show the output window only if unrecognized errors occur.
"showOutput": "silent",
// we run the custom script "compile" as defined in package.json
"args": ["run", "compile"],
// The tsc compiler is started in watching mode
"isWatching": true,
// use the standard tsc in watch mode problem matcher to find compile problems in the output.
"problemMatcher": "$tsc-watch"
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 {window, workspace, DecorationOptions, DecorationRenderOptions, Disposable, Range} from 'vscode';
let decorationType: DecorationRenderOptions = {
before: {
contentText: ' ',
border: 'solid 0.1em #000',
margin: '0.1em 0.2em 0 0.2em',
width: '0.8em',
height: '0.8em'
},
dark: {
before: {
border: 'solid 0.1em #eee'
}
}
};
export function activateColorDecorations(decoratorProvider: (uri: string) => Thenable<Range[]>, supportedLanguages: { [id: string]: boolean }): Disposable {
let disposables: Disposable[] = [];
let colorsDecorationType = window.createTextEditorDecorationType(decorationType);
disposables.push(colorsDecorationType);
let activeEditor = window.activeTextEditor;
if (activeEditor) {
triggerUpdateDecorations();
}
window.onDidChangeActiveTextEditor(editor => {
activeEditor = editor;
if (editor && supportedLanguages[activeEditor.document.languageId]) {
triggerUpdateDecorations();
}
}, null, disposables);
workspace.onDidChangeTextDocument(event => {
if (activeEditor && event.document === activeEditor.document && supportedLanguages[activeEditor.document.languageId]) {
triggerUpdateDecorations();
}
}, null, disposables);
let timeout = null;
function triggerUpdateDecorations() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(updateDecorations, 500);
}
function updateDecorations() {
if (!activeEditor) {
return;
}
let document = activeEditor.document;
if (!supportedLanguages[document.languageId]) {
return;
}
let uri = activeEditor.document.uri.toString();
decoratorProvider(uri).then(ranges => {
let decorations = ranges.map(range => {
let color = document.getText(range);
return <DecorationOptions>{
range: range,
renderOptions: {
before: {
backgroundColor: color
}
}
};
});
activeEditor.setDecorations(colorsDecorationType, decorations);
});
}
return Disposable.from(...disposables);
}
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import {languages, window, commands, ExtensionContext} from 'vscode';
import {LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, Range, TextEdit, Protocol2Code} from 'vscode-languageclient';
import {activateColorDecorations} from './colorDecorators';
namespace ColorSymbolRequest {
export const type: RequestType<string, Range[], any> = { get method() { return 'css/colorSymbols'; } };
}
// this method is called when vs code is activated
export function activate(context: ExtensionContext) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(path.join('server', 'out', 'cssServerMain.js'));
// The debug options for the server
let debugOptions = { execArgv: ['--nolazy', '--debug=6004'] };
// If the extension is launch in debug mode the debug server options are use
// Otherwise the run options are used
let serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
};
// Options to control the language client
let clientOptions: LanguageClientOptions = {
documentSelector: ['css', 'less', 'scss'],
synchronize: {
configurationSection: ['css', 'scss', 'less']
},
initializationOptions: {
}
};
// Create the language client and start the client.
let client = new LanguageClient('css', serverOptions, clientOptions);
let disposable = client.start();
// Push the disposable to the context's subscriptions so that the
// client can be deactivated on extension deactivation
context.subscriptions.push(disposable);
let colorRequestor = (uri: string) => {
return client.sendRequest(ColorSymbolRequest.type, uri).then(ranges => ranges.map(Protocol2Code.asRange));
};
disposable = activateColorDecorations(colorRequestor, { css: true, scss: true, less: true });
context.subscriptions.push(disposable);
languages.setLanguageConfiguration('css', {
wordPattern: /(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g,
comments: {
blockComment: ['/*', '*/']
},
brackets: [['{', '}'], ['[', ']'], ['(', ')']],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '\'', close: '\'', notIn: ['string'] }
]
});
languages.setLanguageConfiguration('less', {
wordPattern: /(#?-?\d*\.\d\w*%?)|([@#!.:]?[\w-?]+%?)|[@#!.]/g,
comments: {
blockComment: ['/*', '*/'],
lineComment: '//'
},
brackets: [['{', '}'], ['[', ']'], ['(', ')'], ['<', '>']],
autoClosingPairs: [
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '[', close: ']', notIn: ['string', 'comment'] },
{ open: '(', close: ')', notIn: ['string', 'comment'] },
{ open: '<', close: '>', notIn: ['string', 'comment'] },
]
});
languages.setLanguageConfiguration('scss', {
wordPattern: /(#?-?\d*\.\d\w*%?)|([@#!.:]?[\w-?]+%?)|[@#!.]/g,
comments: {
blockComment: ['/*', '*/'],
lineComment: '//'
},
brackets: [['{', '}'], ['[', ']'], ['(', ')'], ['<', '>']],
autoClosingPairs: [
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '[', close: ']', notIn: ['string', 'comment'] },
{ open: '(', close: ')', notIn: ['string', 'comment'] },
{ open: '<', close: '>', notIn: ['string', 'comment'] },
]
});
commands.registerCommand('_css.applyCodeAction', applyCodeAction);
}
function applyCodeAction(uri: string, documentVersion: number, edits: TextEdit[]) {
let textEditor = window.activeTextEditor;
if (textEditor && textEditor.document.uri.toString() === uri) {
if (textEditor.document.version !== documentVersion) {
window.showInformationMessage(`CSS fix is outdated and can't be applied to the document.`);
}
textEditor.edit(mutator => {
for (let edit of edits) {
mutator.replace(Protocol2Code.asRange(edit.range), edit.newText);
}
}).then(success => {
if (!success) {
window.showErrorMessage('Failed to apply CSS fix to the document. Please consider opening an issue with steps to reproduce.');
}
});
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../../src/typings/mocha.d.ts'/>
/// <reference path='../../../../../extensions/node.d.ts'/>
/// <reference path='../../../../../extensions/lib.core.d.ts'/>
/// <reference path='../../../../../extensions/declares.d.ts'/>
/// <reference path='../../../node_modules/vscode-languageclient/lib/main.d.ts'/>
\ No newline at end of file
{
"compilerOptions": {
"noLib": true,
"target": "es5",
"module": "commonjs",
"outDir": "./out"
},
"exclude": [
"node_modules"
]
}
\ No newline at end of file
......@@ -2,7 +2,20 @@
"name": "css",
"version": "0.1.0",
"publisher": "vscode",
"engines": { "vscode": "*" },
"engines": {
"vscode": "0.10.x"
},
"activationEvents": [
"onLanguage:css",
"onLanguage:less",
"onLanguage:sass",
"onCommand:_css.applyCodeAction"
],
"main": "./client/out/cssMain",
"scripts": {
"compile": "gulp compile-extension:css-client && gulp compile-extension:css-server",
"postinstall": "cd server && npm install"
},
"contributes": {
"languages": [{
"id": "css",
......@@ -18,6 +31,531 @@
"snippets": [{
"language": "css",
"path": "./snippets/css.json"
}]
}],
"configuration": {
"allOf": [{
"id": "css",
"order": 20,
"title": "CSS configuration",
"allOf": [
{
"title": "Controls CSS validation and problem severities.",
"properties": {
"css.validate": {
"type": "boolean",
"default": true,
"description": "Enables or disables all validations"
},
"css.lint.compatibleVendorPrefixes": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties"
},
"css.lint.vendorPrefix": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "warning",
"description": "When using a vendor-specific prefix also include the standard property"
},
"css.lint.duplicateProperties": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Do not use duplicate style definitions"
},
"css.lint.emptyRules": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "warning",
"description": "Do not use empty rulesets"
},
"css.lint.importStatement": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Import statements do not load in parallel"
},
"css.lint.boxModel": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Do not use width or height when using padding or border"
},
"css.lint.universalSelector": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "The universal selector (*) is known to be slow"
},
"css.lint.zeroUnits": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "No unit for zero needed"
},
"css.lint.fontFaceProperties": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "warning",
"description": "@font-face rule must define 'src' and 'font-family' properties"
},
"css.lint.hexColorLength": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "error",
"description": "Hex colors must consist of three or six hex numbers"
},
"css.lint.argumentsInColorFunction": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "error",
"description": "Invalid number of parameters"
},
"css.lint.unknownProperties": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "warning",
"description": "Unknown property."
},
"css.lint.ieHack": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "IE hacks are only necessary when supporting IE7 and older"
},
"css.lint.unknownVendorSpecificProperties": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Unknown vendor specific property."
},
"css.lint.propertyIgnoredDueToDisplay": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "warning",
"description": "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect"
},
"css.lint.important": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored."
},
"css.lint.float": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes."
},
"css.lint.idSelector": {
"type": "string",
"enum": [ "ignore", "warning", "error"],
"default": "ignore",
"description": "Selectors should not contain IDs because these rules are too tightly coupled with the HTML."
}
}
}
]
},
{
"id": "scss",
"order": 24,
"title": "SCSS (Sass) configuration",
"allOf": [
{
"title": "Controls SCSS validation and problem severities.",
"properties": {
"scss.validate": {
"type": "boolean",
"default": true,
"description": "Enables or disables all validations"
},
"scss.lint.compatibleVendorPrefixes": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties"
},
"scss.lint.vendorPrefix": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "When using a vendor-specific prefix also include the standard property"
},
"scss.lint.duplicateProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Do not use duplicate style definitions"
},
"scss.lint.emptyRules": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "Do not use empty rulesets"
},
"scss.lint.importStatement": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Import statements do not load in parallel"
},
"scss.lint.boxModel": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Do not use width or height when using padding or border"
},
"scss.lint.universalSelector": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "The universal selector (*) is known to be slow"
},
"scss.lint.zeroUnits": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "No unit for zero needed"
},
"scss.lint.fontFaceProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "@font-face rule must define 'src' and 'font-family' properties"
},
"scss.lint.hexColorLength": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"description": "Hex colors must consist of three or six hex numbers"
},
"scss.lint.argumentsInColorFunction": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"description": "Invalid number of parameters"
},
"scss.lint.unknownProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "Unknown property."
},
"scss.lint.ieHack": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "IE hacks are only necessary when supporting IE7 and older"
},
"scss.lint.unknownVendorSpecificProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Unknown vendor specific property."
},
"scss.lint.propertyIgnoredDueToDisplay": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect"
},
"scss.lint.important": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored."
},
"scss.lint.float": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes."
},
"scss.lint.idSelector": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Selectors should not contain IDs because these rules are too tightly coupled with the HTML."
}
}
}
]
},
{
"id": "less",
"order": 22,
"type": "object",
"title": "LESS configuration",
"allOf": [
{
"title": "Controls LESS validation and problem severities.",
"properties": {
"less.validate": {
"type": "boolean",
"default": true,
"description": "Enables or disables all validations"
},
"less.lint.compatibleVendorPrefixes": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "When using a vendor-specific prefix make sure to also include all other vendor-specific properties"
},
"less.lint.vendorPrefix": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "When using a vendor-specific prefix also include the standard property"
},
"less.lint.duplicateProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Do not use duplicate style definitions"
},
"less.lint.emptyRules": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "Do not use empty rulesets"
},
"less.lint.importStatement": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Import statements do not load in parallel"
},
"less.lint.boxModel": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Do not use width or height when using padding or border"
},
"less.lint.universalSelector": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "The universal selector (*) is known to be slow"
},
"less.lint.zeroUnits": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "No unit for zero needed"
},
"less.lint.fontFaceProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "@font-face rule must define 'src' and 'font-family' properties"
},
"less.lint.hexColorLength": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"description": "Hex colors must consist of three or six hex numbers"
},
"less.lint.argumentsInColorFunction": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "error",
"description": "Invalid number of parameters"
},
"less.lint.unknownProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "Unknown property."
},
"less.lint.ieHack": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "IE hacks are only necessary when supporting IE7 and older"
},
"less.lint.unknownVendorSpecificProperties": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Unknown vendor specific property."
},
"less.lint.propertyIgnoredDueToDisplay": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "warning",
"description": "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect"
},
"less.lint.important": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored."
},
"less.lint.float": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes."
},
"less.lint.idSelector": {
"type": "string",
"enum": [
"ignore",
"warning",
"error"
],
"default": "ignore",
"description": "Selectors should not contain IDs because these rules are too tightly coupled with the HTML."
}
}
}
]
}]
}
},
"dependencies": {
"vscode-languageclient": "^2.2.1"
}
}
\ No newline at end of file
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
"configurations": [
{
"name": "Attach",
"type": "node",
"request": "attach",
"port": 6004,
"sourceMaps": true,
"outDir": "${workspaceRoot}/out"
},
{
"name": "Unit Tests",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/../../../node_modules/mocha/bin/_mocha",
"stopOnEntry": false,
"args": [
"--timeout",
"999999",
"--colors"
],
"cwd": "${workspaceRoot}",
"runtimeExecutable": null,
"runtimeArgs": [],
"env": {},
"sourceMaps": true,
"outDir": "${workspaceRoot}/out"
}
]
}
\ No newline at end of file
{
"version": "0.1.0",
"command": "npm",
"isShellCommand": true,
"showOutput": "silent",
"args": ["run", "watch"],
"isWatching": true,
"problemMatcher": "$tsc-watch"
}
\ No newline at end of file
{
"name": "vscode-css-server",
"description": "CSS/LESS/SCSS language server",
"version": "0.0.1",
"author": "Microsoft Corporation",
"license": "MIT",
"engines": {
"node": "*"
},
"dependencies": {
"vscode-languageserver": "^2.2.0",
"vscode-nls": "^1.0.4"
},
"scripts": {
"compile": "gulp compile-extension:css-server",
"watch": "gulp watch-extension:css-server"
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 {
IPCMessageReader, IPCMessageWriter, createConnection, IConnection, Range,
TextDocuments, TextDocument, InitializeParams, InitializeResult, RequestType
} from 'vscode-languageserver';
import {Parser} from './parser/cssParser';
import {CSSCompletion} from './services/cssCompletion';
import {CSSHover} from './services/cssHover';
import {CSSNavigation} from './services/cssNavigation';
import {CSSCodeActions} from './services/cssCodeActions';
import {CSSValidation, Settings} from './services/cssValidation';
import {Stylesheet} from './parser/cssNodes';
import * as nls from 'vscode-nls';
nls.config(process.env['VSCODE_NLS_CONFIG']);
namespace ColorSymbolRequest {
export const type: RequestType<string, Range[], any> = { get method() { return 'css/colorSymbols'; } };
}
// Create a connection for the server. The connection uses for
// stdin / stdout for message passing
let connection: IConnection = createConnection(new IPCMessageReader(process), new IPCMessageWriter(process));
// Create a simple text document manager. The text document manager
// supports full document sync only
let documents: TextDocuments = new TextDocuments();
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// After the server has started the client sends an initilize request. The server receives
// in the passed params the rootPath of the workspace plus the client capabilites.
connection.onInitialize((params: InitializeParams): InitializeResult => {
return {
capabilities: {
// Tell the client that the server works in FULL text document sync mode
textDocumentSync: documents.syncKind,
completionProvider: { resolveProvider: false },
hoverProvider: true,
documentSymbolProvider: true,
referencesProvider: true,
definitionProvider: true,
documentHighlightProvider: true,
codeActionProvider: true
}
};
});
let cssCompletion = new CSSCompletion();
let cssHover = new CSSHover();
let cssValidation = new CSSValidation();
let cssNavigation = new CSSNavigation();
let cssCodeActions = new CSSCodeActions();
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent((change) => {
validateTextDocument(change.document);
});
// The settings have changed. Is send on server activation as well.
connection.onDidChangeConfiguration((change) => {
updateConfiguration(<Settings>change.settings);
});
function updateConfiguration(settings: Settings) {
cssValidation.configure(settings.css);
// Revalidate any open text documents
documents.all().forEach(validateTextDocument);
}
function validateTextDocument(textDocument: TextDocument): void {
if (textDocument.getText().length === 0) {
// ignore empty documents
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
return;
}
let stylesheet = getStylesheet(textDocument);
cssValidation.doValidation(textDocument, stylesheet).then(diagnostics => {
// Send the computed diagnostics to VSCode.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});
}
let parser = new Parser();
function getStylesheet(document: TextDocument): Stylesheet {
return parser.parseStylesheet(document);
}
connection.onCompletion(textDocumentPosition => {
let document = documents.get(textDocumentPosition.textDocument.uri);
let stylesheet = getStylesheet(document);
return cssCompletion.doComplete(document, textDocumentPosition.position, stylesheet);
});
connection.onHover(textDocumentPosition => {
let document = documents.get(textDocumentPosition.textDocument.uri);
let styleSheet = getStylesheet(document);
return cssHover.doHover(document, textDocumentPosition.position, styleSheet);
});
connection.onDocumentSymbol(documentSymbolParams => {
let document = documents.get(documentSymbolParams.textDocument.uri);
let stylesheet = getStylesheet(document);
return cssNavigation.findDocumentSymbols(document, stylesheet);
});
connection.onDefinition(documentSymbolParams => {
let document = documents.get(documentSymbolParams.textDocument.uri);
let stylesheet = getStylesheet(document);
return cssNavigation.findDefinition(document, documentSymbolParams.position, stylesheet);
});
connection.onDocumentHighlight(documentSymbolParams => {
let document = documents.get(documentSymbolParams.textDocument.uri);
let stylesheet = getStylesheet(document);
return cssNavigation.findDocumentHighlights(document, documentSymbolParams.position, stylesheet);
});
connection.onReferences(referenceParams => {
let document = documents.get(referenceParams.textDocument.uri);
let stylesheet = getStylesheet(document);
return cssNavigation.findReferences(document, referenceParams.position, stylesheet);
});
connection.onCodeAction(codeActionParams => {
let document = documents.get(codeActionParams.textDocument.uri);
let stylesheet = getStylesheet(document);
return cssCodeActions.doCodeActions(document, codeActionParams.range, codeActionParams.context, stylesheet);
});
connection.onRequest(ColorSymbolRequest.type, uri => {
let document = documents.get(uri);
let stylesheet = getStylesheet(document);
return cssNavigation.findColorSymbols(document, stylesheet);
});
// Listen on the connection
connection.listen();
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export declare var data: any;
export declare var descriptions: any;
\ No newline at end of file
因为 它太大了无法显示 source diff 。你可以改为 查看blob
因为 它太大了无法显示 source diff 。你可以改为 查看blob
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* global __dirname */
var fs = require('fs');
var path = require('path');
var xml2js = require('xml2js');
var os = require('os');
var util = require('util');
// keep in sync with data from language facts
var colors = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgrey: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
grey: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370d8',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#d87093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
red: '#ff0000',
rebeccapurple: '#663399',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32'
};
var otherColors = {
"ActiveBorder": "Active window border.",
"ActiveCaption": "Active window caption.",
"AppWorkspace": "Background color of multiple document interface.",
"Background": "Desktop background.",
"ButtonFace": "The face background color for 3-D elements that appear 3-D due to one layer of surrounding border.",
"ButtonHighlight": "The color of the border facing the light source for 3-D elements that appear 3-D due to one layer of surrounding border.",
"ButtonShadow": "The color of the border away from the light source for 3-D elements that appear 3-D due to one layer of surrounding border.",
"ButtonText": "Text on push buttons.",
"CaptionText": "Text in caption, size box, and scrollbar arrow box.",
"currentColor": "The value of the 'color' property. The computed value of the 'currentColor' keyword is the computed value of the 'color' property. If the 'currentColor' keyword is set on the 'color' property itself, it is treated as 'color:inherit' at parse time.",
"GrayText": "Grayed (disabled) text. This color is set to #000 if the current display driver does not support a solid gray color.",
"Highlight": "Item(s) selected in a control.",
"HighlightText": "Text of item(s) selected in a control.",
"InactiveBorder": "Inactive window border.",
"InactiveCaption": "Inactive window caption.",
"InactiveCaptionText": "Color of text in an inactive caption.",
"InfoBackground": "Background color for tooltip controls.",
"InfoText": "Text color for tooltip controls.",
"Menu": "Menu background.",
"MenuText": "Text in menus.",
"Scrollbar": "Scroll bar gray area.",
"ThreeDDarkShadow": "The color of the darker (generally outer) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",
"ThreeDFace": "The face background color for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",
"ThreeDHighlight": "The color of the lighter (generally outer) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",
"ThreeDLightShadow": "The color of the darker (generally inner) of the two borders facing the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",
"ThreeDShadow": "The color of the lighter (generally inner) of the two borders away from the light source for 3-D elements that appear 3-D due to two concentric layers of surrounding border.",
"transparent": "Fully transparent. This keyword can be considered a shorthand for rgba(0,0,0,0) which is its computed value.",
"Window": "Window background.",
"WindowFrame": "Window frame.",
"WindowText": "Text in windows.",
"none": "",
//ignore these
"-webkit-activelink": "",
"-webkit-focus-ring-color": '',
"-webkit-link": '',
"-webkit-text": ''
};
function clone(obj) {
var copy = {};
for (var i in obj) {
copy[i] = obj[i];
}
return copy;
}
function getProperties(obj) {
var res = [];
for (var i in obj) {
res.push(i);
}
return res;
}
function getValues(valArr, restriction, ruleName) {
if (!Array.isArray(valArr)) {
if (valArr.$) {
valArr = [ valArr ];
} else {
return [];
}
}
var vals = valArr.map(function (v) {
return {
name: v.$.name,
desc: v.desc,
browsers: v.$.browsers !== 'all' ? v.$.browsers : void 0
};
}).filter(function (v) {
if (v.browsers === 'none') {
return false;
}
return true;
});
if (restriction.indexOf('color') !== -1) {
var colorsCopy = clone(colors);
var otherColorsCopy = clone(otherColors);
var moreColors = {};
vals = vals.filter(function (v) {
if (typeof colorsCopy[v.name] === 'string') {
delete colorsCopy[v.name];
return false;
}
if (typeof otherColorsCopy[v.name] === 'string') {
delete otherColorsCopy[v.name];
return false;
}
moreColors[v.name] = v.desc;
return true;
});
var notCovered = [];
for (var i in colorsCopy) {
notCovered.push(i);
}
for (var i in otherColorsCopy) {
notCovered.push(i);
}
if (notCovered.length > 0) {
console.log('***' + ruleName + ' uncovered: ' + notCovered.length); // + ' - ' + JSON.stringify(notCovered));
}
if (restriction === 'color') {
var properties = getProperties(moreColors);
console.log('---' + ruleName + ' others : ' + properties.length); // + ' - ' + JSON.stringify(properties));
}
}
return vals;
}
function internalizeDescriptions(entries) {
var descriptions = {};
var conflicts = {};
entries.forEach(function (e) {
if (e.values) {
e.values.forEach(function (d) {
if (!d.desc) {
conflicts[d.name] = true;
return;
}
var existing = descriptions[d.name];
if (existing) {
if (existing !== d.desc) {
conflicts[d.name] = true;
}
}
descriptions[d.name] = d.desc;
});
}
});
entries.forEach(function (e) {
if (e.values) {
e.values.forEach(function (d) {
if (!conflicts[d.name]) {
delete d.desc;
} else {
delete descriptions[d.name];
}
});
}
});
return descriptions;
}
function toSource(object, keyName) {
if (!object.css[keyName]) {
return [];
}
var result = [];
var entryArr = object.css[keyName].entry;
entryArr.forEach(function (e) {
if (e.$.browsers === 'none') {
return;
}
var data = {
name: e.$.name,
desc: e.desc,
browsers: e.$.browsers !== 'all' ? e.$.browsers : void 0
};
if (e.$.restriction) {
data.restriction= e.$.restriction;
}
if (e.values) {
data.values= getValues(e.values.value, data.restriction || '', data.name);
}
result.push(data);
});
return result;
}
var parser = new xml2js.Parser({explicitArray : false});
var schemaFileName= 'css-schema.xml';
fs.readFile(path.resolve(__dirname, schemaFileName), function(err, data) {
parser.parseString(data, function (err, result) {
//console.log(util.inspect(result, {depth: null})); //Work
var atdirectives = toSource(result, 'atDirectives');
var pseudoclasses = toSource(result, 'pseudoClasses');
var pseudoelements = toSource(result, 'pseudoElements');
var properties = toSource(result, 'properties');
var descriptions = internalizeDescriptions([].concat(atdirectives, pseudoclasses, pseudoelements, properties));
var resultObject = {
css: {
atdirectives: atdirectives,
pseudoclasses: pseudoclasses,
pseudoelements: pseudoelements,
properties: properties
}
};
var output = [
'/*---------------------------------------------------------------------------------------------',
' * Copyright (c) Microsoft Corporation. All rights reserved.',
' * Licensed under the MIT License. See License.txt in the project root for license information.',
' *--------------------------------------------------------------------------------------------*/',
'// file generated from ' + schemaFileName + ' using css-exclude_generate_browserjs.js',
'',
'(function (factory) {',
'\tif (typeof module === "object" && typeof module.exports === "object") {',
'\t\tvar v = factory(require, exports); if (v !== undefined) module.exports = v;',
'\t} else if (typeof define === "function" && define.amd) {',
'\t\tdefine(["require", "exports"], factory);',
'\t}',
'})(function (require, exports) {',
'\texports.data = ' + JSON.stringify(resultObject, null, '\t') + ';',
'\texports.descriptions = ' + JSON.stringify(descriptions, null, '\t') + ';',
'});'
];
var outputPath = path.resolve(__dirname, '../browsers.js');
console.log('Writing to: ' + outputPath);
var content = output.join(os.EOL);
fs.writeFileSync(outputPath, content);
console.log('Done');
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 CSSIssueType implements nodes.IRule {
id: string;
message: string;
public constructor(id: string, message: string) {
this.id = id;
this.message = message;
}
}
export let ParseError = {
NumberExpected: new CSSIssueType('css-numberexpected', localize('expected.number', "number expected")),
ConditionExpected: new CSSIssueType('css-conditionexpected', localize('expected.condt', "condition expected")),
RuleOrSelectorExpected: new CSSIssueType('css-ruleorselectorexpected', localize('expected.ruleorselector', "at-rule or selector expected")),
DotExpected: new CSSIssueType('css-dotexpected', localize('expected.dot', "dot expected")),
ColonExpected: new CSSIssueType('css-colonexpected', localize('expected.colon', "colon expected")),
SemiColonExpected: new CSSIssueType('css-semicolonexpected', localize('expected.semicolon', "semi-colon expected")),
TermExpected: new CSSIssueType('css-termexpected', localize('expected.term', "term expected")),
ExpressionExpected: new CSSIssueType('css-expressionexpected', localize('expected.expression', "expression expected")),
OperatorExpected: new CSSIssueType('css-operatorexpected', localize('expected.operator', "operator expected")),
IdentifierExpected: new CSSIssueType('css-identifierexpected', localize('expected.ident', "identifier expected")),
PercentageExpected: new CSSIssueType('css-percentageexpected', localize('expected.percentage', "percentage expected")),
URIOrStringExpected: new CSSIssueType('css-uriorstringexpected', localize('expected.uriorstring', "uri or string expected")),
URIExpected: new CSSIssueType('css-uriexpected', localize('expected.uri', "URI expected")),
VariableNameExpected: new CSSIssueType('css-varnameexpected', localize('expected.varname', "variable name expected")),
VariableValueExpected: new CSSIssueType('css-varvalueexpected', localize('expected.varvalue', "variable value expected")),
PropertyValueExpected: new CSSIssueType('css-propertyvalueexpected', localize('expected.propvalue', "property value expected")),
LeftCurlyExpected: new CSSIssueType('css-lcurlyexpected', localize('expected.lcurly', "{ expected")),
RightCurlyExpected: new CSSIssueType('css-rcurlyexpected', localize('expected.rcurly', "} expected")),
LeftSquareBracketExpected: new CSSIssueType('css-rbracketexpected', localize('expected.lsquare', "[ expected")),
RightSquareBracketExpected: new CSSIssueType('css-lbracketexpected', localize('expected.rsquare', "] expected")),
LeftParenthesisExpected: new CSSIssueType('css-lparentexpected', localize('expected.lparen', "( expected")),
RightParenthesisExpected: new CSSIssueType('css-rparentexpected', localize('expected.rparent', ") expected")),
CommaExpected: new CSSIssueType('css-commaexpected', localize('expected.comma', "comma expected")),
PageDirectiveOrDeclarationExpected: new CSSIssueType('css-pagedirordeclexpected', localize('expected.pagedirordecl', "page directive or declaraton expected")),
UnknownAtRule: new CSSIssueType('css-unknownatrule', localize('unknown.atrule', "at-rule unknown")),
UnknownKeyword: new CSSIssueType('css-unknownkeyword', localize('unknown.keyword', "unknown keyword")),
SelectorExpected: new CSSIssueType('css-selectorexpected', localize('expected.selector', "selector expected")),
StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', localize('expected.stringliteral', "string literal expected")),
};
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
/// <summary>
/// Nodes for the css 2.1 specification. See for reference:
/// http://www.w3.org/TR/CSS21/grammar.html#grammar
/// </summary>
export enum NodeType {
Undefined,
Identifier,
Stylesheet,
Ruleset,
Selector,
SimpleSelector,
SelectorInterpolation,
SelectorCombinator,
SelectorCombinatorParent,
SelectorCombinatorSibling,
SelectorCombinatorAllSiblings,
Page,
PageBoxMarginBox,
ClassSelector,
IdentifierSelector,
ElementNameSelector,
PseudoSelector,
AttributeSelector,
Declaration,
Declarations,
Property,
Expression,
BinaryExpression,
Term,
Operator,
Value,
StringLiteral,
URILiteral,
EscapedValue,
Function,
NumericValue,
HexColorValue,
MixinDeclaration,
MixinReference,
VariableName,
VariableDeclaration,
Prio,
Interpolation,
NestedProperties,
ExtendsReference,
SelectorPlaceholder,
Debug,
If,
Else,
For,
Each,
While,
MixinContent,
Media,
Keyframe,
FontFace,
Import,
Namespace,
Invocation,
FunctionDeclaration,
ReturnStatement,
MediaQuery,
FunctionParameter,
FunctionArgument,
KeyframeSelector,
ViewPort,
Document
}
export enum ReferenceType {
Mixin,
Rule,
Variable,
Function,
Keyframe,
Unknown
}
export function getNodeAtOffset(node: Node, offset: number): Node {
let candidate: Node = null;
if (!node || offset < node.offset || offset > node.end) {
return null;
}
// Find the shortest node at the position
node.accept((node) => {
if (node.offset === -1 && node.length === -1) {
return true;
}
if (node.offset <= offset && node.end >= offset) {
if (!candidate) {
candidate = node;
} else if (node.length <= candidate.length) {
candidate = node;
}
return true;
}
return false;
});
return candidate;
}
export function getNodePath(node: Node, offset: number): Node[] {
let candidate: Node = getNodeAtOffset(node, offset),
path: Node[] = [];
while (candidate) {
path.unshift(candidate);
candidate = candidate.parent;
}
return path;
}
export function getParentDeclaration(node: Node): Declaration {
let decl = <Declaration>node.findParent(NodeType.Declaration);
if (decl && decl.getValue() && decl.getValue().encloses(node)) {
return decl;
}
return null;
}
export interface ITextProvider {
(offset: number, length: number): string;
}
export class Node {
public parent: Node;
public offset: number;
public length: number;
public get end() { return this.offset + this.length; };
public options: { [name: string]: any; };
public textProvider: ITextProvider; // only set on the root node
private children: Node[];
private issues: IMarker[];
private nodeType: NodeType;
constructor(offset: number = -1, len: number = -1, nodeType?: NodeType) {
this.parent = null;
this.offset = offset;
this.length = len;
if (nodeType) {
this.nodeType = nodeType;
}
}
public set type(type: NodeType) {
this.nodeType = type;
}
public get type(): NodeType {
return this.nodeType || NodeType.Undefined;
}
public getTextProvider(): ITextProvider {
let node: Node = this;
while (node && !node.textProvider) {
node = node.parent;
}
if (node) {
return node.textProvider;
}
return () => { return 'unknown'; };
}
public getText(): string {
return this.getTextProvider()(this.offset, this.length);
}
public matches(str: string): boolean {
return this.length === str.length && this.getTextProvider()(this.offset, this.length) === str;
}
public startsWith(str: string): boolean {
return this.length >= str.length && this.getTextProvider()(this.offset, str.length) === str;
}
public endsWith(str: string): boolean {
return this.length >= str.length && this.getTextProvider()(this.end - str.length, str.length) === str;
}
public accept(visitor: IVisitorFunction): void;
public accept(visitor: IVisitor): void;
public accept(visitor: any): void {
if (typeof visitor !== 'function') {
visitor = visitor.visitNode.bind(visitor);
}
if (visitor(this) && this.children) {
this.children.forEach((child) => {
child.accept(visitor);
});
}
}
public adoptChild(node: Node, index: number = -1): Node {
if (node.parent && node.parent.children) {
let idx = node.parent.children.indexOf(node);
if (idx >= 0) {
node.parent.children.splice(idx, 1);
}
}
node.parent = this;
let children = this.children;
if (!children) {
children = this.children = [];
}
if (index !== -1) {
children.splice(index, 0, node);
} else {
children.push(node);
}
return node;
}
public attachTo(parent: Node, index: number = -1): Node {
if (parent) {
parent.adoptChild(this, index);
}
return this;
}
public collectIssues(results: any[]): void {
if (this.issues) {
results.push.apply(results, this.issues);
}
}
public addIssue(issue: IMarker): void {
if (!this.issues) {
this.issues = [];
}
this.issues.push(issue);
}
public hasIssue(rule: IRule): boolean {
return this.issues && this.issues.some(i => i.getRule() === rule);
}
public isErroneous(): boolean {
return this.issues && this.issues.length > 0;
}
public setNode(field: string, node: Node, index: number = -1): boolean {
if (node) {
node.attachTo(this, index);
this[field] = node;
return true;
}
return false;
}
public addChild(node: Node): boolean {
if (node) {
if (!this.children) {
this.children = [];
}
node.attachTo(this);
this.updateOffsetAndLength(node);
return true;
}
return false;
}
private updateOffsetAndLength(node: Node): void {
if (node.offset < this.offset || this.offset === -1) {
this.offset = node.offset;
}
let nodeEnd = node.end;
if ((nodeEnd > this.end) || this.length === -1) {
this.length = nodeEnd - this.offset;
}
}
public hasChildren(): boolean {
return this.children && this.children.length > 0;
}
public getChildren(): Node[] {
return this.children ? this.children.slice(0) : [];
}
public getChild(index: number): Node {
if (this.children && index < this.children.length) {
return this.children[index];
}
return null;
}
public addChildren(nodes: Node[]): void {
nodes.forEach((node) => this.addChild(node));
}
public findFirstChildBeforeOffset(offset: number): Node {
if (this.children) {
let current: Node = null;
for (let i = this.children.length - 1; i >= 0; i--) {
// iterate until we find a child that has a start offset smaller than the input offset
current = this.children[i];
if (current.offset <= offset) {
return current;
}
}
}
return null;
}
public findChildAtOffset(offset: number, goDeep: boolean): Node {
let current: Node = this.findFirstChildBeforeOffset(offset);
if (current && current.end >= offset) {
if (goDeep) {
return current.findChildAtOffset(offset, true) || current;
}
return current;
}
return null;
}
public encloses(candidate: Node): boolean {
return this.offset <= candidate.offset && this.offset + this.length >= candidate.offset + candidate.length;
}
public getParent(): Node {
let result = this.parent;
while (result instanceof Nodelist) {
result = result.parent;
}
return result;
}
public findParent(type: NodeType): Node {
let result: Node = this;
while (result && result.type !== type) {
result = result.parent;
}
return result;
}
public setData(key: string, value: any): void {
if (!this.options) {
this.options = {};
}
this.options[key] = value;
}
public getData(key: string): any {
if (!this.options || !this.options.hasOwnProperty(key)) {
return null;
}
return this.options[key];
}
}
export class Nodelist extends Node {
constructor(parent: Node, index: number = -1) {
super(-1, -1);
this.attachTo(parent, index);
this.offset = -1;
this.length = -1;
}
}
export class Identifier extends Node {
public referenceTypes: ReferenceType[];
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Identifier;
}
public containsInterpolation(): boolean {
return this.hasChildren();
}
}
export class Stylesheet extends Node {
private name: string;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Stylesheet;
}
public setName(value: string): void {
this.name = value;
}
}
export class Declarations extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Declarations;
}
}
export class BodyDeclaration extends Node {
private declarations: Declarations;
constructor(offset: number, length: number) {
super(offset, length);
}
public getDeclarations(): Declarations {
return this.declarations;
}
public setDeclarations(decls: Declarations): boolean {
return this.setNode('declarations', decls);
}
}
export class RuleSet extends BodyDeclaration {
private selectors: Nodelist;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Ruleset;
}
public getSelectors(): Nodelist {
if (!this.selectors) {
this.selectors = new Nodelist(this);
}
return this.selectors;
}
public isNested(): boolean {
return this.parent && this.parent.findParent(NodeType.Ruleset) !== null;
}
}
export class Selector extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Selector;
}
}
export class SimpleSelector extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.SimpleSelector;
}
}
export abstract class AbstractDeclaration extends Node {
// positions for code assist
public colonPosition: number;
public semicolonPosition: number; // semicolon following the declaration
constructor(offset: number, length: number) {
super(offset, length);
}
}
export class Declaration extends AbstractDeclaration {
private property: Property;
private value: Expression;
private nestedProprties: NestedProperties;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Declaration;
}
public setProperty(node: Property): boolean {
return this.setNode('property', node);
}
public getProperty(): Property {
return this.property;
}
public getFullPropertyName(): string {
let propertyName = this.property ? this.property.getName() : 'unknown';
if (this.parent instanceof Declarations && this.parent.getParent() instanceof NestedProperties) {
let parentDecl = this.parent.getParent().getParent();
if (parentDecl instanceof Declaration) {
return (<Declaration>parentDecl).getFullPropertyName() + propertyName;
}
}
return propertyName;
}
public getNonPrefixedPropertyName(): string {
let propertyName = this.getFullPropertyName();
if (propertyName && propertyName.charAt(0) === '-') {
let vendorPrefixEnd = propertyName.indexOf('-', 1);
if (vendorPrefixEnd !== -1) {
return propertyName.substring(vendorPrefixEnd + 1);
}
}
return propertyName;
}
public setValue(value: Expression): boolean {
return this.setNode('value', value);
}
public getValue(): Expression {
return this.value;
}
public setNestedProperties(value: NestedProperties): boolean {
return this.setNode('nestedProprties', value);
}
public getNestedProperties(): NestedProperties {
return this.nestedProprties;
}
}
export class Property extends Node {
private identifier: Identifier;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Property;
}
public setIdentifier(value: Identifier): boolean {
return this.setNode('identifier', value);
}
public getIdentifier(): Identifier {
return this.identifier;
}
public getName(): string {
return this.getText();
}
}
export class Invocation extends Node {
private arguments: Nodelist;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Invocation;
}
public getArguments(): Nodelist {
if (!this.arguments) {
this.arguments = new Nodelist(this);
}
return this.arguments;
}
}
export class Function extends Invocation {
private identifier: Identifier;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Function;
}
public setIdentifier(node: Identifier): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Identifier {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
}
export class FunctionParameter extends Node {
private identifier: Node;
private defaultValue: Node;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.FunctionParameter;
}
public setIdentifier(node: Node): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Node {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
public setDefaultValue(node: Node): boolean {
return this.setNode('defaultValue', node, 0);
}
public getDefaultValue(): Node {
return this.defaultValue;
}
}
export class FunctionArgument extends Node {
private identifier: Node;
private value: Node;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.FunctionArgument;
}
public setIdentifier(node: Node): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Node {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
public setValue(node: Node): boolean {
return this.setNode('value', node, 0);
}
public getValue(): Node {
return this.value;
}
}
export class IfStatement extends BodyDeclaration {
public expression: Expression;
public elseClause: BodyDeclaration;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.If;
}
public setExpression(node: Expression): boolean {
return this.setNode('expression', node, 0);
}
public setElseClause(elseClause: BodyDeclaration): boolean {
return this.setNode('elseClause', elseClause);
}
}
export class ForStatement extends BodyDeclaration {
public variable: Variable;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.For;
}
public setVariable(node: Variable): boolean {
return this.setNode('variable', node, 0);
}
}
export class EachStatement extends BodyDeclaration {
public variable: Variable;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Each;
}
public setVariable(node: Variable): boolean {
return this.setNode('variable', node, 0);
}
}
export class WhileStatement extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.While;
}
}
export class ElseStatement extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Else;
}
}
export class FunctionDeclaration extends BodyDeclaration {
private identifier: Identifier;
private parameters: Nodelist;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.FunctionDeclaration;
}
public setIdentifier(node: Identifier): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Identifier {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
public getParameters(): Nodelist {
if (!this.parameters) {
this.parameters = new Nodelist(this);
}
return this.parameters;
}
}
export class ViewPort extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.ViewPort;
}
}
export class FontFace extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.FontFace;
}
}
export class NestedProperties extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.NestedProperties;
}
}
export class Keyframe extends BodyDeclaration {
private keyword: Node;
private identifier: Identifier;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Keyframe;
}
public setKeyword(keyword: Node): boolean {
return this.setNode('keyword', keyword, 0);
}
public getKeyword(): Node {
return this.keyword;
}
public setIdentifier(node: Identifier): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Identifier {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
}
export class KeyframeSelector extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.KeyframeSelector;
}
}
export class Import extends Node {
private medialist: Node;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Import;
}
public setMedialist(node: Node): boolean {
if (node) {
node.attachTo(this);
this.medialist = node;
return true;
}
return false;
}
}
export class Namespace extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Namespace;
}
}
export class Media extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Media;
}
}
export class Document extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Document;
}
}
export class Medialist extends Node {
private mediums: Nodelist;
constructor(offset: number, length: number) {
super(offset, length);
}
public getMediums(): Nodelist {
if (!this.mediums) {
this.mediums = new Nodelist(this);
}
return this.mediums;
}
}
export class MediaQuery extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.MediaQuery;
}
}
export class Page extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Page;
}
}
export class PageBoxMarginBox extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.PageBoxMarginBox;
}
}
export class Expression extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Expression;
}
}
export class BinaryExpression extends Node {
private left: Node;
private right: Node;
private operator: Node;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.BinaryExpression;
}
public setLeft(left: Node): boolean {
return this.setNode('left', left);
}
public getLeft(): Node {
return this.left;
}
public setRight(right: Node): boolean {
return this.setNode('right', right);
}
public getRight(): Node {
return this.right;
}
public setOperator(value: Node): boolean {
return this.setNode('operator', value);
}
public getOperator(): Node {
return this.operator;
}
}
export class Term extends Node {
private operator: Node;
private expression: Node;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Term;
}
public setOperator(value: Node): boolean {
return this.setNode('operator', value);
}
public getOperator(): Node {
return this.operator;
}
public setExpression(value: Node): boolean {
return this.setNode('expression', value);
}
public getExpression(): Node {
return this.expression;
}
}
export class Operator extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Operator;
}
}
export class HexColorValue extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.HexColorValue;
}
}
export class NumericValue extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.NumericValue;
}
public getValue(): { value: string; unit: string } {
let raw = this.getText();
let unitIdx = 0,
code: number,
_dot = '.'.charCodeAt(0),
_0 = '0'.charCodeAt(0),
_9 = '9'.charCodeAt(0);
for (let i = 0, len = raw.length; i < len; i++) {
code = raw.charCodeAt(i);
if (!(_0 <= code && code <= _9 || code === _dot)) {
break;
}
unitIdx += 1;
}
return {
value: raw.substring(0, unitIdx),
unit: unitIdx < raw.length ? raw.substring(unitIdx) : undefined
};
}
}
export class VariableDeclaration extends AbstractDeclaration {
private variable: Variable;
private value: Node;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.VariableDeclaration;
}
public setVariable(node: Variable): boolean {
if (node) {
node.attachTo(this);
this.variable = node;
return true;
}
return false;
}
public getVariable(): Variable {
return this.variable;
}
public getName(): string {
return this.variable ? this.variable.getName() : '';
}
public setValue(node: Node): boolean {
if (node) {
node.attachTo(this);
this.value = node;
return true;
}
return false;
}
}
export class Interpolation extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.Interpolation;
}
}
export class Variable extends Node {
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.VariableName;
}
public getName(): string {
return this.getText();
}
}
export class ExtendsReference extends Node {
private selector: Selector;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.ExtendsReference;
}
public setSelector(node: Selector): boolean {
return this.setNode('selector', node, 0);
}
public getSelector(): Selector {
return this.selector;
}
public getName(): string {
return this.selector ? this.selector.getText() : '';
}
}
export class MixinReference extends Node {
private identifier: Identifier;
private arguments: Nodelist;
private content: BodyDeclaration;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.MixinReference;
}
public setIdentifier(node: Identifier): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Identifier {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
public getArguments(): Nodelist {
if (!this.arguments) {
this.arguments = new Nodelist(this);
}
return this.arguments;
}
public setContent(node: BodyDeclaration): boolean {
return this.setNode('content', node);
}
public getContent(): BodyDeclaration {
return this.content;
}
}
export class MixinDeclaration extends BodyDeclaration {
private identifier: Identifier;
private parameters: Nodelist;
private guard: LessGuard;
constructor(offset: number, length: number) {
super(offset, length);
}
public get type(): NodeType {
return NodeType.MixinDeclaration;
}
public setIdentifier(node: Identifier): boolean {
return this.setNode('identifier', node, 0);
}
public getIdentifier(): Identifier {
return this.identifier;
}
public getName(): string {
return this.identifier ? this.identifier.getText() : '';
}
public getParameters(): Nodelist {
if (!this.parameters) {
this.parameters = new Nodelist(this);
}
return this.parameters;
}
public setGuard(node: LessGuard): boolean {
if (node) {
node.attachTo(this);
this.guard = node;
}
return false;
}
}
export class LessGuard extends Node {
public isNegated: boolean;
private conditions: Nodelist;
public getConditions(): Nodelist {
if (!this.conditions) {
this.conditions = new Nodelist(this);
}
return this.conditions;
}
}
export class GuardCondition extends Node {
public variable: Node;
public isEquals: boolean;
public isGreater: boolean;
public isEqualsGreater: boolean;
public isLess: boolean;
public isEqualsLess: boolean;
public setVariable(node: Node): boolean {
return this.setNode('variable', node);
}
}
export interface IRule {
id: string;
message: string;
}
export enum Level {
Ignore = 1,
Warning = 2,
Error = 4
}
export interface IMarker {
getNode(): Node;
getMessage(): string;
getOffset(): number;
getLength(): number;
getRule(): IRule;
getLevel(): Level;
}
export class Marker implements IMarker {
private node: Node;
private rule: IRule;
private level: Level;
private message: string;
private offset: number;
private length: number;
constructor(node: Node, rule: IRule, level: Level, message?: string, offset: number = node.offset, length: number = node.length) {
this.node = node;
this.rule = rule;
this.level = level;
this.message = message || rule.message;
this.offset = offset;
this.length = length;
}
public getRule(): IRule {
return this.rule;
}
public getLevel(): Level {
return this.level;
}
public getOffset(): number {
return this.offset;
}
public getLength(): number {
return this.length;
}
public getNode(): Node {
return this.node;
}
public getMessage(): string {
return this.message;
}
}
export interface IVisitor {
visitNode: (node: Node) => boolean;
}
export interface IVisitorFunction {
(node: Node): boolean;
}
/*
export class DefaultVisitor implements IVisitor {
public visitNode(node:Node):boolean {
switch (node.type) {
case NodeType.Stylesheet:
return this.visitStylesheet(<Stylesheet> node);
case NodeType.FontFace:
return this.visitFontFace(<FontFace> node);
case NodeType.Ruleset:
return this.visitRuleSet(<RuleSet> node);
case NodeType.Selector:
return this.visitSelector(<Selector> node);
case NodeType.SimpleSelector:
return this.visitSimpleSelector(<SimpleSelector> node);
case NodeType.Declaration:
return this.visitDeclaration(<Declaration> node);
case NodeType.Function:
return this.visitFunction(<Function> node);
case NodeType.FunctionDeclaration:
return this.visitFunctionDeclaration(<FunctionDeclaration> node);
case NodeType.FunctionParameter:
return this.visitFunctionParameter(<FunctionParameter> node);
case NodeType.FunctionArgument:
return this.visitFunctionArgument(<FunctionArgument> node);
case NodeType.Term:
return this.visitTerm(<Term> node);
case NodeType.Declaration:
return this.visitExpression(<Expression> node);
case NodeType.NumericValue:
return this.visitNumericValue(<NumericValue> node);
case NodeType.Page:
return this.visitPage(<Page> node);
case NodeType.PageBoxMarginBox:
return this.visitPageBoxMarginBox(<PageBoxMarginBox> node);
case NodeType.Property:
return this.visitProperty(<Property> node);
case NodeType.NumericValue:
return this.visitNodelist(<Nodelist> node);
case NodeType.Import:
return this.visitImport(<Import> node);
case NodeType.Namespace:
return this.visitNamespace(<Namespace> node);
case NodeType.Keyframe:
return this.visitKeyframe(<Keyframe> node);
case NodeType.KeyframeSelector:
return this.visitKeyframeSelector(<KeyframeSelector> node);
case NodeType.MixinDeclaration:
return this.visitMixinDeclaration(<MixinDeclaration> node);
case NodeType.MixinReference:
return this.visitMixinReference(<MixinReference> node);
case NodeType.Variable:
return this.visitVariable(<Variable> node);
case NodeType.VariableDeclaration:
return this.visitVariableDeclaration(<VariableDeclaration> node);
}
return this.visitUnknownNode(node);
}
public visitFontFace(node:FontFace):boolean {
return true;
}
public visitKeyframe(node:Keyframe):boolean {
return true;
}
public visitKeyframeSelector(node:KeyframeSelector):boolean {
return true;
}
public visitStylesheet(node:Stylesheet):boolean {
return true;
}
public visitProperty(Node:Property):boolean {
return true;
}
public visitRuleSet(node:RuleSet):boolean {
return true;
}
public visitSelector(node:Selector):boolean {
return true;
}
public visitSimpleSelector(node:SimpleSelector):boolean {
return true;
}
public visitDeclaration(node:Declaration):boolean {
return true;
}
public visitFunction(node:Function):boolean {
return true;
}
public visitFunctionDeclaration(node:FunctionDeclaration):boolean {
return true;
}
public visitInvocation(node:Invocation):boolean {
return true;
}
public visitTerm(node:Term):boolean {
return true;
}
public visitImport(node:Import):boolean {
return true;
}
public visitNamespace(node:Namespace):boolean {
return true;
}
public visitExpression(node:Expression):boolean {
return true;
}
public visitNumericValue(node:NumericValue):boolean {
return true;
}
public visitPage(node:Page):boolean {
return true;
}
public visitPageBoxMarginBox(node:PageBoxMarginBox):boolean {
return true;
}
public visitNodelist(node:Nodelist):boolean {
return true;
}
public visitVariableDeclaration(node:VariableDeclaration):boolean {
return true;
}
public visitVariable(node:Variable):boolean {
return true;
}
public visitMixinDeclaration(node:MixinDeclaration):boolean {
return true;
}
public visitMixinReference(node:MixinReference):boolean {
return true;
}
public visitUnknownNode(node:Node):boolean {
return true;
}
}
*/
export class ParseErrorCollector implements IVisitor {
static entries(node: Node): IMarker[] {
let visitor = new ParseErrorCollector();
node.accept(visitor);
return visitor.entries;
}
public entries: IMarker[];
constructor() {
this.entries = [];
}
public visitNode(node: Node): boolean {
if (node.isErroneous()) {
node.collectIssues(this.entries);
}
return true;
}
}
/*---------------------------------------------------------------------------------------------
* 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 scanner = require('./cssScanner');
import nodes = require('./cssNodes');
import errors = require('./cssErrors');
import languageFacts = require('../services/languageFacts');
import {TextDocument} from 'vscode-languageserver';
export interface IMark {
prev: scanner.IToken;
curr: scanner.IToken;
pos: number;
}
/// <summary>
/// A parser for the css core specification. See for reference:
/// http://www.w3.org/TR/CSS21/syndata.html#tokenization
/// </summary>
export class Parser {
public scanner: scanner.Scanner;
public token: scanner.IToken;
public prevToken: scanner.IToken;
private lastErrorToken: scanner.IToken;
constructor(scnr: scanner.Scanner = new scanner.Scanner()) {
this.scanner = scnr;
this.token = null;
this.prevToken = null;
}
public peek(type: scanner.TokenType, text?: string, ignoreCase: boolean = true): boolean {
if (type !== this.token.type) {
return false;
}
if (typeof text !== 'undefined') {
if (ignoreCase) {
return text.toLowerCase() === this.token.text.toLowerCase();
} else {
return text === this.token.text;
}
}
return true;
}
public peekRegExp(type: scanner.TokenType, regEx: RegExp): boolean {
if (type !== this.token.type) {
return false;
}
return regEx.test(this.token.text);
}
public hasWhitespace(): boolean {
return this.prevToken && (this.prevToken.offset + this.prevToken.len !== this.token.offset);
}
public consumeToken(): void {
this.prevToken = this.token;
this.token = this.scanner.scan();
}
public mark(): IMark {
return {
prev: this.prevToken,
curr: this.token,
pos: this.scanner.pos()
};
}
public restoreAtMark(mark: IMark): void {
this.prevToken = mark.prev;
this.token = mark.curr;
this.scanner.goBackTo(mark.pos);
}
public acceptOne(type: scanner.TokenType, text?: string[], ignoreCase: boolean = true): boolean {
for (let i = 0; i < text.length; i++) {
if (this.peek(type, text[i], ignoreCase)) {
this.consumeToken();
return true;
}
}
return false;
}
public accept(type: scanner.TokenType, text?: string, ignoreCase: boolean = true): boolean {
if (this.peek(type, text, ignoreCase)) {
this.consumeToken();
return true;
}
return false;
}
public resync(resyncTokens: scanner.TokenType[], resyncStopTokens: scanner.TokenType[]): boolean {
while (true) {
if (resyncTokens && resyncTokens.indexOf(this.token.type) !== -1) {
this.consumeToken();
return true;
} else if (resyncStopTokens && resyncStopTokens.indexOf(this.token.type) !== -1) {
return true;
} else {
if (this.token.type === scanner.TokenType.EOF) {
return false;
}
this.token = this.scanner.scan();
}
}
}
public createNode(nodeType: nodes.NodeType): nodes.Node {
return new nodes.Node(this.token.offset, this.token.len, nodeType);
}
public create(ctor: any): nodes.Node {
let obj = Object.create(ctor.prototype);
ctor.apply(obj, [this.token.offset, this.token.len]);
return obj;
}
public finish<T extends nodes.Node>(node: T, error?: errors.CSSIssueType, resyncTokens?: scanner.TokenType[], resyncStopTokens?: scanner.TokenType[]): T {
// parseNumeric misuses error for boolean flagging (however the real error mustn't be a false)
// + nodelist offsets mustn't be modified, because there is a offset hack in rulesets for smartselection
if (!(node instanceof nodes.Nodelist)) {
if (error) {
this.markError(node, error, resyncTokens, resyncStopTokens);
}
// set the node end position
if (this.prevToken !== null) {
// length with more elements belonging together
let prevEnd = this.prevToken.offset + this.prevToken.len;
node.length = prevEnd > node.offset ? prevEnd - node.offset : 0; // offset is taken from current token, end from previous: Use 0 for empty nodes
}
}
return node;
}
public markError<T extends nodes.Node>(node: T, error: errors.CSSIssueType, resyncTokens?: scanner.TokenType[], resyncStopTokens?: scanner.TokenType[]): void {
if (this.token !== this.lastErrorToken) { // do not report twice on the same token
node.addIssue(new nodes.Marker(node, error, nodes.Level.Error, null, this.token.offset, this.token.len));
this.lastErrorToken = this.token;
}
if (resyncTokens || resyncStopTokens) {
this.resync(resyncTokens, resyncStopTokens);
}
}
public parseStylesheet(textDocument: TextDocument): nodes.Stylesheet {
let versionId = textDocument.version;
let textProvider = (offset: number, length: number) => {
if (textDocument.version !== versionId) {
throw new Error('Underlying model has changed, AST is no longer valid');
}
return textDocument.getText().substr(offset, length);
};
return this.internalParse(textDocument.getText(), this._parseStylesheet, textProvider);
}
public internalParse<T extends nodes.Node>(input: string, parseFunc: () => T, textProvider?: nodes.ITextProvider): T {
this.scanner.setSource(input);
this.token = this.scanner.scan();
let node = parseFunc.bind(this)();
if (node) {
if (textProvider) {
node.textProvider = textProvider;
} else {
node.textProvider = (offset: number, length: number) => { return input.substr(offset, length); };
}
}
return node;
}
public _parseStylesheet(): nodes.Stylesheet {
let node = <nodes.Stylesheet>this.create(nodes.Stylesheet);
node.addChild(this._parseCharset());
let inRecovery = false;
do {
let hasMatch = false;
do {
hasMatch = false;
let statement = this._parseStylesheetStatement();
if (statement) {
node.addChild(statement);
hasMatch = true;
inRecovery = false;
if (!this.peek(scanner.TokenType.EOF) && this._needsSemicolonAfter(statement) && !this.accept(scanner.TokenType.SemiColon)) {
this.markError(node, errors.ParseError.SemiColonExpected);
}
}
while (this.accept(scanner.TokenType.SemiColon) || this.accept(scanner.TokenType.CDO) || this.accept(scanner.TokenType.CDC)) {
// accept empty statements
hasMatch = true;
inRecovery = false;
}
} while (hasMatch);
if (this.peek(scanner.TokenType.EOF)) {
break;
}
if (!inRecovery) {
if (this.peek(scanner.TokenType.AtKeyword)) {
this.markError(node, errors.ParseError.UnknownAtRule);
} else {
this.markError(node, errors.ParseError.RuleOrSelectorExpected);
}
inRecovery = true;
}
this.consumeToken();
} while (!this.peek(scanner.TokenType.EOF));
return this.finish(node);
}
public _parseStylesheetStatement(): nodes.Node {
return this._parseRuleset(false)
|| this._parseImport()
|| this._parseMedia()
|| this._parsePage()
|| this._parseFontFace()
|| this._parseKeyframe()
|| this._parseViewPort()
|| this._parseNamespace()
|| this._parseDocument();
}
public _tryParseRuleset(isNested: boolean): nodes.RuleSet {
let mark = this.mark();
if (this._parseSelector(isNested)) {
while (this.accept(scanner.TokenType.Comma) && this._parseSelector(isNested)) {
// loop
}
if (this.accept(scanner.TokenType.CurlyL)) {
this.restoreAtMark(mark);
return this._parseRuleset(isNested);
}
}
this.restoreAtMark(mark);
return null;
}
public _parseRuleset(isNested: boolean = false): nodes.RuleSet {
let node = <nodes.RuleSet>this.create(nodes.RuleSet);
if (!node.getSelectors().addChild(this._parseSelector(isNested))) {
return null;
}
while (this.accept(scanner.TokenType.Comma) && node.getSelectors().addChild(this._parseSelector(isNested))) {
// loop
}
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseRuleSetDeclaration(): nodes.Node {
return this._parseDeclaration();
}
public _needsSemicolonAfter(node: nodes.Node): boolean {
switch (node.type) {
case nodes.NodeType.Keyframe:
case nodes.NodeType.ViewPort:
case nodes.NodeType.Media:
case nodes.NodeType.Ruleset:
case nodes.NodeType.Namespace:
case nodes.NodeType.If:
case nodes.NodeType.For:
case nodes.NodeType.Each:
case nodes.NodeType.While:
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.FunctionDeclaration:
return false;
case nodes.NodeType.VariableDeclaration:
case nodes.NodeType.ExtendsReference:
case nodes.NodeType.MixinContent:
case nodes.NodeType.ReturnStatement:
case nodes.NodeType.MediaQuery:
case nodes.NodeType.Debug:
case nodes.NodeType.Import:
return true;
case nodes.NodeType.MixinReference:
return !(<nodes.MixinReference>node).getContent();
case nodes.NodeType.Declaration:
return !(<nodes.Declaration>node).getNestedProperties();
}
return false;
}
public _parseDeclarations(parseDeclaration: () => nodes.Node): nodes.Declarations {
let node = <nodes.Declarations>this.create(nodes.Declarations);
if (!this.accept(scanner.TokenType.CurlyL)) {
return null;
}
let decl = parseDeclaration();
while (node.addChild(decl)) {
if (this.peek(scanner.TokenType.CurlyR)) {
break;
}
if (this._needsSemicolonAfter(decl) && !this.accept(scanner.TokenType.SemiColon)) {
return this.finish(node, errors.ParseError.SemiColonExpected, [scanner.TokenType.SemiColon, scanner.TokenType.CurlyR]);
}
while (this.accept(scanner.TokenType.SemiColon)) {
// accept empty statements
}
decl = parseDeclaration();
}
if (!this.accept(scanner.TokenType.CurlyR)) {
return this.finish(node, errors.ParseError.RightCurlyExpected, [scanner.TokenType.CurlyR, scanner.TokenType.SemiColon]);
}
return this.finish(node);
}
public _parseBody<T extends nodes.BodyDeclaration>(node: T, parseDeclaration: () => nodes.Node): T {
if (!node.setDeclarations(this._parseDeclarations(parseDeclaration))) {
return this.finish(node, errors.ParseError.LeftCurlyExpected, [scanner.TokenType.CurlyR, scanner.TokenType.SemiColon]);
}
return this.finish(node);
}
public _parseSelector(isNested: boolean): nodes.Selector {
let node = <nodes.Selector>this.create(nodes.Selector);
let hasContent = false;
if (isNested) {
// nested selectors can start with a combinator
hasContent = node.addChild(this._parseCombinator());
}
while (node.addChild(this._parseSimpleSelector())) {
hasContent = true;
node.addChild(this._parseCombinator()); // optional
}
return hasContent ? this.finish(node) : null;
}
public _parseDeclaration(resyncStopTokens?: scanner.TokenType[]): nodes.Declaration {
let node = <nodes.Declaration>this.create(nodes.Declaration);
if (!node.setProperty(this._parseProperty())) {
return null;
}
if (!this.accept(scanner.TokenType.Colon)) {
return <nodes.Declaration>this.finish(node, errors.ParseError.ColonExpected, [scanner.TokenType.Colon], resyncStopTokens);
}
node.colonPosition = this.prevToken.offset;
if (!node.setValue(this._parseExpr())) {
return this.finish(node, errors.ParseError.PropertyValueExpected);
}
node.addChild(this._parsePrio());
if (this.peek(scanner.TokenType.SemiColon)) {
node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist
}
return this.finish(node);
}
public _tryToParseDeclaration(): nodes.Declaration {
let mark = this.mark();
if (this._parseProperty() && this.accept(scanner.TokenType.Colon)) {
// looks like a declaration, go ahead
this.restoreAtMark(mark);
return this._parseDeclaration();
}
this.restoreAtMark(mark);
return null;
}
public _parseProperty(): nodes.Property {
let node = <nodes.Property>this.create(nodes.Property);
let mark = this.mark();
if (this.accept(scanner.TokenType.Delim, '*') || this.accept(scanner.TokenType.Delim, '_')) {
// support for IE 5.x, 6 and 7 star hack: see http://en.wikipedia.org/wiki/CSS_filter#Star_hack
if (this.hasWhitespace()) {
this.restoreAtMark(mark);
return null;
}
}
if (node.setIdentifier(this._parseIdent())) {
return <nodes.Property>this.finish(node);
}
return null;
}
public _parseCharset(): nodes.Node {
let node = this.create(nodes.Node);
if (!this.accept(scanner.TokenType.Charset)) {
return null;
}
if (!this.accept(scanner.TokenType.String)) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
if (!this.accept(scanner.TokenType.SemiColon)) {
return this.finish(node, errors.ParseError.SemiColonExpected);
}
return this.finish(node);
}
public _parseImport(): nodes.Node {
let node = <nodes.Import>this.create(nodes.Import);
if (!this.accept(scanner.TokenType.AtKeyword, '@import')) {
return null;
}
if (!this.accept(scanner.TokenType.URI) && !this.accept(scanner.TokenType.String)) {
return this.finish(node, errors.ParseError.URIOrStringExpected);
}
node.setMedialist(this._parseMediaList());
return this.finish(node);
}
public _parseNamespace(): nodes.Node {
// http://www.w3.org/TR/css3-namespace/
// namespace : NAMESPACE_SYM S* [IDENT S*]? [STRING|URI] S* ';' S*
let node = <nodes.Namespace>this.create(nodes.Namespace);
if (!this.accept(scanner.TokenType.AtKeyword, '@namespace')) {
return null;
}
node.addChild(this._parseIdent()); // optional prefix
if (!this.accept(scanner.TokenType.URI) && !this.accept(scanner.TokenType.String)) {
return this.finish(node, errors.ParseError.URIExpected, [scanner.TokenType.SemiColon]);
}
if (!this.accept(scanner.TokenType.SemiColon)) {
return this.finish(node, errors.ParseError.SemiColonExpected);
}
return this.finish(node);
}
public _parseFontFace(): nodes.Node {
if (!this.peek(scanner.TokenType.AtKeyword, '@font-face')) {
return null;
}
let node = <nodes.FontFace>this.create(nodes.FontFace);
this.consumeToken(); // @font-face
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseViewPort(): nodes.Node {
if (!this.peek(scanner.TokenType.AtKeyword, '@-ms-viewport') &&
!this.peek(scanner.TokenType.AtKeyword, '@-o-viewport') &&
!this.peek(scanner.TokenType.AtKeyword, '@viewport')
) {
return null;
}
let node = <nodes.ViewPort>this.create(nodes.ViewPort);
this.consumeToken(); // @-ms-viewport
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseKeyframe(): nodes.Node {
let node = <nodes.Keyframe>this.create(nodes.Keyframe);
let atNode = this.create(nodes.Node);
if (!this.accept(scanner.TokenType.AtKeyword, '@keyframes') &&
!this.accept(scanner.TokenType.AtKeyword, '@-webkit-keyframes') &&
!this.accept(scanner.TokenType.AtKeyword, '@-ms-keyframes') &&
!this.accept(scanner.TokenType.AtKeyword, '@-moz-keyframes') &&
!this.accept(scanner.TokenType.AtKeyword, '@-o-keyframes')) {
return null;
}
node.setKeyword(this.finish(atNode));
if (atNode.getText() === '@-ms-keyframes') { // -ms-keyframes never existed
this.markError(atNode, errors.ParseError.UnknownKeyword);
}
if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Keyframe]))) {
return this.finish(node, errors.ParseError.IdentifierExpected, [scanner.TokenType.CurlyR]);
}
return this._parseBody(node, this._parseKeyframeSelector.bind(this));
}
public _parseKeyframeSelector(): nodes.Node {
let node = <nodes.KeyframeSelector>this.create(nodes.KeyframeSelector);
if (!node.addChild(this._parseIdent()) && !this.accept(scanner.TokenType.Percentage)) {
return null;
}
while (this.accept(scanner.TokenType.Comma)) {
if (!node.addChild(this._parseIdent()) && !this.accept(scanner.TokenType.Percentage)) {
return this.finish(node, errors.ParseError.PercentageExpected);
}
}
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parseMediaDeclaration(): nodes.Node {
return this._tryParseRuleset(false) || this._tryToParseDeclaration() || this._parseStylesheetStatement();
}
public _parseMedia(): nodes.Node {
// MEDIA_SYM S* media_query_list '{' S* ruleset* '}' S*
// media_query_list : S* [media_query [ ',' S* media_query ]* ]?
let node = <nodes.Media>this.create(nodes.Media);
if (!this.accept(scanner.TokenType.AtKeyword, '@media')) {
return null;
}
if (!node.addChild(this._parseMediaQuery([scanner.TokenType.CurlyL]))) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
while (this.accept(scanner.TokenType.Comma)) {
if (!node.addChild(this._parseMediaQuery([scanner.TokenType.CurlyL]))) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
}
return this._parseBody(node, this._parseMediaDeclaration.bind(this));
}
public _parseMediaQuery(resyncStopToken: scanner.TokenType[]): nodes.Node {
// http://www.w3.org/TR/css3-mediaqueries/
// media_query : [ONLY | NOT]? S* IDENT S* [ AND S* expression ]* | expression [ AND S* expression ]*
// expression : '(' S* IDENT S* [ ':' S* expr ]? ')' S*
let node = <nodes.MediaQuery>this.create(nodes.MediaQuery);
let parseExpression = true;
let hasContent = false;
if (!this.peek(scanner.TokenType.ParenthesisL)) {
if (this.accept(scanner.TokenType.Ident, 'only', true) || this.accept(scanner.TokenType.Ident, 'not', true)) {
// optional
}
if (!node.addChild(this._parseIdent())) {
return null;
}
hasContent = true;
parseExpression = this.accept(scanner.TokenType.Ident, 'and', true);
}
while (parseExpression) {
if (!this.accept(scanner.TokenType.ParenthesisL)) {
if (hasContent) {
return this.finish(node, errors.ParseError.LeftParenthesisExpected, [], resyncStopToken);
}
return null;
}
if (!node.addChild(this._parseMediaFeatureName())) {
return this.finish(node, errors.ParseError.IdentifierExpected, [], resyncStopToken);
}
if (this.accept(scanner.TokenType.Colon)) {
if (!node.addChild(this._parseExpr())) {
return this.finish(node, errors.ParseError.TermExpected, [], resyncStopToken);
}
}
if (!this.accept(scanner.TokenType.ParenthesisR)) {
return this.finish(node, errors.ParseError.RightParenthesisExpected, [], resyncStopToken);
}
parseExpression = this.accept(scanner.TokenType.Ident, 'and', true);
}
return node;
}
public _parseMediaFeatureName(): nodes.Node {
return this._parseIdent();
}
public _parseMediaList(): nodes.Medialist {
let node = <nodes.Medialist>this.create(nodes.Medialist);
if (node.getMediums().addChild(this._parseMedium())) {
while (this.accept(scanner.TokenType.Comma)) {
if (!node.getMediums().addChild(this._parseMedium())) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
}
return <nodes.Medialist>this.finish(node);
}
return null;
}
public _parseMedium(): nodes.Node {
let node = this.create(nodes.Node);
if (node.addChild(this._parseIdent())) {
return this.finish(node);
} else {
return null;
}
}
public _parsePageDeclaration(): nodes.Node {
return this._parsePageMarginBox() || this._parseRuleSetDeclaration();
}
public _parsePage(): nodes.Node {
// http://www.w3.org/TR/css3-page/
// page_rule : PAGE_SYM S* page_selector_list '{' S* page_body '}' S*
// page_body : /* Can be empty */ declaration? [ ';' S* page_body ]? | page_margin_box page_body
let node = <nodes.Page>this.create(nodes.Page);
if (!this.accept(scanner.TokenType.AtKeyword, '@Page')) {
return null;
}
if (node.addChild(this._parsePageSelector())) {
while (this.accept(scanner.TokenType.Comma)) {
if (!node.addChild(this._parsePageSelector())) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
}
}
return this._parseBody(node, this._parsePageDeclaration.bind(this));
}
public _parsePageMarginBox(): nodes.Node {
// page_margin_box : margin_sym S* '{' S* declaration? [ ';' S* declaration? ]* '}' S*
let node = <nodes.PageBoxMarginBox>this.create(nodes.PageBoxMarginBox);
if (!this.peek(scanner.TokenType.AtKeyword)) {
return null;
}
if (!this.acceptOne(scanner.TokenType.AtKeyword, languageFacts.getPageBoxDirectives())) {
this.markError(node, errors.ParseError.UnknownAtRule, [], [scanner.TokenType.CurlyL]);
}
return this._parseBody(node, this._parseRuleSetDeclaration.bind(this));
}
public _parsePageSelector(): nodes.Node {
// page_selector : pseudo_page+ | IDENT pseudo_page*
// pseudo_page : ':' [ "left" | "right" | "first" | "blank" ];
let node = this.create(nodes.Node);
if (!this.peek(scanner.TokenType.Ident) && !this.peek(scanner.TokenType.Colon)) {
return null;
}
node.addChild(this._parseIdent()); // optional ident
if (this.accept(scanner.TokenType.Colon)) {
if (!node.addChild(this._parseIdent())) { // optional ident
return this.finish(node, errors.ParseError.IdentifierExpected);
}
}
return this.finish(node);
}
public _parseDocument(): nodes.Node {
// -moz-document is experimental but has been pushed to css4
let node = <nodes.Document>this.create(nodes.Document);
if (!this.accept(scanner.TokenType.AtKeyword, '@-moz-document')) {
return null;
}
this.resync([], [scanner.TokenType.CurlyL]); // ignore all the rules
return this._parseBody(node, this._parseStylesheetStatement.bind(this));
}
public _parseOperator(): nodes.Node {
// these are operators for binary expressions
let node = this.createNode(nodes.NodeType.Operator);
if (this.accept(scanner.TokenType.Delim, '/') ||
this.accept(scanner.TokenType.Delim, '*') ||
this.accept(scanner.TokenType.Delim, '+') ||
this.accept(scanner.TokenType.Delim, '-') ||
this.accept(scanner.TokenType.Dashmatch) ||
this.accept(scanner.TokenType.Includes) ||
this.accept(scanner.TokenType.SubstringOperator) ||
this.accept(scanner.TokenType.PrefixOperator) ||
this.accept(scanner.TokenType.SuffixOperator) ||
this.accept(scanner.TokenType.Delim, '=')) { // doesn't stick to the standard here
return this.finish(node);
} else {
return null;
}
}
public _parseUnaryOperator(): nodes.Node {
let node = this.create(nodes.Node);
if (this.accept(scanner.TokenType.Delim, '+') || this.accept(scanner.TokenType.Delim, '-')) {
return this.finish(node);
} else {
return null;
}
}
public _parseCombinator(): nodes.Node {
let node = this.create(nodes.Node);
if (this.accept(scanner.TokenType.Delim, '>')) {
node.type = nodes.NodeType.SelectorCombinatorParent;
return this.finish(node);
} else if (this.accept(scanner.TokenType.Delim, '+')) {
node.type = nodes.NodeType.SelectorCombinatorSibling;
return this.finish(node);
} else if (this.accept(scanner.TokenType.Delim, '~')) {
node.type = nodes.NodeType.SelectorCombinatorAllSiblings;
return this.finish(node);
} else {
return null;
}
}
public _parseSimpleSelector(): nodes.Node {
// simple_selector
// : element_name [ HASH | class | attrib | pseudo ]* | [ HASH | class | attrib | pseudo ]+ ;
let node = <nodes.SimpleSelector>this.create(nodes.SimpleSelector);
let c = 0;
if (node.addChild(this._parseElementName())) {
c++;
}
while ((c === 0 || !this.hasWhitespace()) && node.addChild(this._parseSimpleSelectorBody())) {
c++;
}
return c > 0 ? this.finish(node) : null;
}
public _parseSimpleSelectorBody(): nodes.Node {
return this._parsePseudo() || this._parseHash() || this._parseClass() || this._parseAttrib();
}
public _parseSelectorIdent(): nodes.Node {
return this._parseIdent();
}
public _parseHash(): nodes.Node {
if (!this.peek(scanner.TokenType.Hash) && !this.peek(scanner.TokenType.Delim, '#')) {
return null;
}
let node = this.createNode(nodes.NodeType.IdentifierSelector);
if (this.accept(scanner.TokenType.Delim, '#')) {
if (this.hasWhitespace() || !node.addChild(this._parseSelectorIdent())) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
} else {
this.consumeToken(); // TokenType.Hash
}
return this.finish(node);
}
public _parseClass(): nodes.Node {
// class: '.' IDENT ;
if (!this.peek(scanner.TokenType.Delim, '.')) {
return null;
}
let node = this.createNode(nodes.NodeType.ClassSelector);
this.consumeToken(); // '.'
if (this.hasWhitespace() || !node.addChild(this._parseSelectorIdent())) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
return this.finish(node);
}
public _parseElementName(): nodes.Node {
// element_name: IDENT | '*';
let node = this.createNode(nodes.NodeType.ElementNameSelector);
if (node.addChild(this._parseSelectorIdent()) || this.accept(scanner.TokenType.Delim, '*')) {
return this.finish(node);
}
return null;
}
public _parseAttrib(): nodes.Node {
// attrib : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* [ IDENT | STRING ] S* ]? ']'
if (!this.peek(scanner.TokenType.BracketL)) {
return null;
}
let node = this.createNode(nodes.NodeType.AttributeSelector);
this.consumeToken(); // BracketL
if (!node.addChild(this._parseBinaryExpr())) {
// is this bad?
}
if (!this.accept(scanner.TokenType.BracketR)) {
return this.finish(node, errors.ParseError.RightSquareBracketExpected);
}
return this.finish(node);
}
public _parsePseudo(): nodes.Node {
// pseudo: ':' [ IDENT | FUNCTION S* [IDENT S*]? ')' ]
if (!this.peek(scanner.TokenType.Colon)) {
return null;
}
let pos = this.mark();
let node = this.createNode(nodes.NodeType.PseudoSelector);
this.consumeToken(); // Colon
if (!this.hasWhitespace() && this.accept(scanner.TokenType.Colon)) {
// optional, support ::
}
if (!this.hasWhitespace()) {
if (!node.addChild(this._parseIdent())) {
return this.finish(node, errors.ParseError.IdentifierExpected);
}
if (!this.hasWhitespace() && this.accept(scanner.TokenType.ParenthesisL)) {
node.addChild(this._parseBinaryExpr() || this._parseSimpleSelector());
if (!this.accept(scanner.TokenType.ParenthesisR)) {
return this.finish(node, errors.ParseError.RightParenthesisExpected);
}
}
return this.finish(node);
}
this.restoreAtMark(pos);
return null;
}
public _parsePrio(): nodes.Node {
if (!this.peek(scanner.TokenType.Exclamation)) {
return null;
}
let node = this.createNode(nodes.NodeType.Prio);
if (this.accept(scanner.TokenType.Exclamation) && this.accept(scanner.TokenType.Ident, 'important', true)) {
return this.finish(node);
}
return null;
}
public _parseExpr(stopOnComma: boolean = false): nodes.Expression {
let node = <nodes.Expression>this.create(nodes.Expression);
if (!node.addChild(this._parseBinaryExpr())) {
return null;
}
while (true) {
if (this.peek(scanner.TokenType.Comma)) { // optional
if (stopOnComma) {
return this.finish(node);
}
this.consumeToken();
}
if (!node.addChild(this._parseBinaryExpr())) {
break;
}
}
return this.finish(node);
}
public _parseBinaryExpr(preparsedLeft?: nodes.BinaryExpression, preparsedOper?: nodes.Node): nodes.Node {
let node = <nodes.BinaryExpression>this.create(nodes.BinaryExpression);
if (!node.setLeft((<nodes.Node>preparsedLeft || this._parseTerm()))) {
return null;
}
if (!node.setOperator(preparsedOper || this._parseOperator())) {
return this.finish(node);
}
if (!node.setRight(this._parseTerm())) {
return this.finish(node, errors.ParseError.TermExpected);
}
// things needed for multiple binary expressions
node = <nodes.BinaryExpression>this.finish(node);
let operator = this._parseOperator();
if (operator) {
node = <nodes.BinaryExpression>this._parseBinaryExpr(node, operator);
}
return this.finish(node);
}
public _parseTerm(): nodes.Term {
let node = <nodes.Term>this.create(nodes.Term);
node.setOperator(this._parseUnaryOperator()); // optional
if (node.setExpression(this._parseFunction()) || // first function then ident
node.setExpression(this._parseIdent()) ||
node.setExpression(this._parseURILiteral()) ||
node.setExpression(this._parseStringLiteral()) ||
node.setExpression(this._parseNumeric()) ||
node.setExpression(this._parseHexColor()) ||
node.setExpression(this._parseOperation())
) {
return <nodes.Term>this.finish(node);
}
return null;
}
public _parseOperation(): nodes.Node {
let node = this.create(nodes.Node);
if (!this.accept(scanner.TokenType.ParenthesisL)) {
return null;
}
node.addChild(this._parseExpr());
if (!this.accept(scanner.TokenType.ParenthesisR)) {
return this.finish(node, errors.ParseError.RightParenthesisExpected);
}
return this.finish(node);
}
public _parseNumeric(): nodes.NumericValue {
let node = <nodes.NumericValue>this.create(nodes.NumericValue);
if (this.accept(scanner.TokenType.Num) ||
this.accept(scanner.TokenType.Percentage) ||
this.accept(scanner.TokenType.Resolution) ||
this.accept(scanner.TokenType.Length) ||
this.accept(scanner.TokenType.EMS) ||
this.accept(scanner.TokenType.EXS) ||
this.accept(scanner.TokenType.Angle) ||
this.accept(scanner.TokenType.Time) ||
this.accept(scanner.TokenType.Dimension) ||
this.accept(scanner.TokenType.Freq)) {
return <nodes.NumericValue>this.finish(node);
}
return null;
}
public _parseStringLiteral(): nodes.Node {
let node = this.createNode(nodes.NodeType.StringLiteral);
if (this.accept(scanner.TokenType.String) || this.accept(scanner.TokenType.BadString)) {
return this.finish(node);
}
return null;
}
public _parseURILiteral(): nodes.Node {
let node = this.createNode(nodes.NodeType.URILiteral);
if (this.accept(scanner.TokenType.URI) || this.accept(scanner.TokenType.BadUri)) {
return this.finish(node);
}
return null;
}
public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier {
let node = <nodes.Identifier>this.create(nodes.Identifier);
if (referenceTypes) {
node.referenceTypes = referenceTypes;
}
if (this.accept(scanner.TokenType.Ident)) {
return this.finish(node);
}
return null;
}
public _parseFunction(): nodes.Function {
let pos = this.mark();
let node = <nodes.Function>this.create(nodes.Function);
if (!node.setIdentifier(this._parseFunctionIdentifier())) {
return null;
}
if (this.hasWhitespace() || !this.accept(scanner.TokenType.ParenthesisL)) {
this.restoreAtMark(pos);
return null;
}
if (node.getArguments().addChild(this._parseFunctionArgument())) {
while (this.accept(scanner.TokenType.Comma)) {
if (!node.getArguments().addChild(this._parseFunctionArgument())) {
this.markError(node, errors.ParseError.ExpressionExpected);
}
}
}
if (!this.accept(scanner.TokenType.ParenthesisR)) {
return <nodes.Function>this.finish(node, errors.ParseError.RightParenthesisExpected);
}
return <nodes.Function>this.finish(node);
}
public _parseFunctionIdentifier(): nodes.Identifier {
let node = <nodes.Identifier>this.create(nodes.Identifier);
node.referenceTypes = [nodes.ReferenceType.Function];
if (this.accept(scanner.TokenType.Ident, 'progid')) {
// support for IE7 specific filters: 'progid:DXImageTransform.Microsoft.MotionBlur(strength=13, direction=310)'
if (this.accept(scanner.TokenType.Colon)) {
while (this.accept(scanner.TokenType.Ident) && this.accept(scanner.TokenType.Delim, '.')) {
// loop
}
}
return this.finish(node);
} else if (this.accept(scanner.TokenType.Ident)) {
return this.finish(node);
}
return null;
}
public _parseFunctionArgument(): nodes.Node {
let node = <nodes.FunctionArgument>this.create(nodes.FunctionArgument);
if (node.setValue(this._parseExpr(true))) {
return this.finish(node);
}
return null;
}
public _parseHexColor(): nodes.Node {
if (this.peekRegExp(scanner.TokenType.Hash, /^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/g)) {
let node = this.create(nodes.HexColorValue);
this.consumeToken();
return this.finish(node);
} else {
return null;
}
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export enum TokenType {
Ident,
AtKeyword,
String,
BadString,
BadUri,
Hash,
Num,
Percentage,
Dimension,
URI,
UnicodeRange,
CDO,
CDC,
Colon,
SemiColon,
CurlyL,
CurlyR,
ParenthesisL,
ParenthesisR,
BracketL,
BracketR,
Whitespace,
Includes,
Dashmatch,
SubstringOperator,
PrefixOperator,
SuffixOperator,
Delim,
EMS,
EXS,
Length,
Angle,
Time,
Freq,
Exclamation,
Resolution,
Comma,
Charset,
EscapedJavaScript,
BadEscapedJavaScript,
Comment,
SingleLineComment,
EOF,
CustomToken
}
export interface IToken {
type: TokenType;
text: string;
offset: number;
len: number;
}
export class MultiLineStream {
private source: string;
private len: number;
private position: number;
constructor(source: string) {
this.source = source;
this.len = source.length;
this.position = 0;
}
public substring(from: number, to: number = this.position): string {
return this.source.substring(from, to);
}
public eos(): boolean {
return this.len <= this.position;
}
public pos(): number {
return this.position;
}
public goBackTo(pos: number): void {
this.position = pos;
}
public goBack(n: number): void {
this.position -= n;
}
public advance(n: number): void {
this.position += n;
}
public nextChar(): number {
return this.source.charCodeAt(this.position++) || 0;
}
public peekChar(n: number = 0): number {
return this.source.charCodeAt(this.position + n) || 0;
}
public lookbackChar(n: number = 0): number {
return this.source.charCodeAt(this.position - n) || 0;
}
public advanceIfChar(ch: number): boolean {
if (ch === this.source.charCodeAt(this.position)) {
this.position++;
return true;
}
return false;
}
public advanceIfChars(ch: number[]): boolean {
let i: number;
if (this.position + ch.length > this.source.length) {
return false;
}
for (i = 0; i < ch.length; i++) {
if (this.source.charCodeAt(this.position + i) !== ch[i]) {
return false;
}
}
this.advance(i);
return true;
}
public advanceWhileChar(condition: (ch: number) => boolean): number {
let posNow = this.position;
while (this.position < this.len && condition(this.source.charCodeAt(this.position))) {
this.position++;
}
return this.position - posNow;
}
}
const _a = 'a'.charCodeAt(0);
const _e = 'e'.charCodeAt(0);
const _f = 'f'.charCodeAt(0);
const _i = 'i'.charCodeAt(0);
const _l = 'l'.charCodeAt(0);
const _p = 'p'.charCodeAt(0);
const _r = 'r'.charCodeAt(0);
const _u = 'u'.charCodeAt(0);
const _x = 'x'.charCodeAt(0);
const _z = 'z'.charCodeAt(0);
const _A = 'A'.charCodeAt(0);
const _E = 'E'.charCodeAt(0);
const _F = 'F'.charCodeAt(0);
const _I = 'I'.charCodeAt(0);
const _L = 'L'.charCodeAt(0);
const _P = 'P'.charCodeAt(0);
const _R = 'R'.charCodeAt(0);
const _U = 'U'.charCodeAt(0);
const _X = 'X'.charCodeAt(0);
const _Z = 'Z'.charCodeAt(0);
const _0 = '0'.charCodeAt(0);
const _9 = '9'.charCodeAt(0);
const _TLD = '~'.charCodeAt(0);
const _HAT = '^'.charCodeAt(0);
const _EQS = '='.charCodeAt(0);
const _PIP = '|'.charCodeAt(0);
const _MIN = '-'.charCodeAt(0);
const _USC = '_'.charCodeAt(0);
const _PRC = '%'.charCodeAt(0);
const _MUL = '*'.charCodeAt(0);
const _LPA = '('.charCodeAt(0);
const _RPA = ')'.charCodeAt(0);
const _LAN = '<'.charCodeAt(0);
const _RAN = '>'.charCodeAt(0);
const _ATS = '@'.charCodeAt(0);
const _HSH = '#'.charCodeAt(0);
const _DLR = '$'.charCodeAt(0);
const _BSL = '\\'.charCodeAt(0);
const _FSL = '/'.charCodeAt(0);
const _NWL = '\n'.charCodeAt(0);
const _CAR = '\r'.charCodeAt(0);
const _LFD = '\f'.charCodeAt(0);
const _DQO = '"'.charCodeAt(0);
const _SQO = '\''.charCodeAt(0);
const _WSP = ' '.charCodeAt(0);
const _TAB = '\t'.charCodeAt(0);
const _SEM = ';'.charCodeAt(0);
const _COL = ':'.charCodeAt(0);
const _CUL = '{'.charCodeAt(0);
const _CUR = '}'.charCodeAt(0);
const _BRL = '['.charCodeAt(0);
const _BRR = ']'.charCodeAt(0);
const _CMA = ','.charCodeAt(0);
const _DOT = '.'.charCodeAt(0);
const _BNG = '!'.charCodeAt(0);
const _url = [_u, _U, _r, _R, _l, _L, _LPA, _LPA];
const _url_prefix = [_u, _U, _r, _R, _l, _L, _MIN, _MIN, _p, _P, _r, _R, _e, _E, _f, _F, _i, _I, _x, _X, _LPA, _LPA];
const staticTokenTable: { [code: number]: TokenType; } = {};
staticTokenTable[_SEM] = TokenType.SemiColon;
staticTokenTable[_COL] = TokenType.Colon;
staticTokenTable[_CUL] = TokenType.CurlyL;
staticTokenTable[_CUR] = TokenType.CurlyR;
staticTokenTable[_BRR] = TokenType.BracketR;
staticTokenTable[_BRL] = TokenType.BracketL;
staticTokenTable[_LPA] = TokenType.ParenthesisL;
staticTokenTable[_RPA] = TokenType.ParenthesisR;
staticTokenTable[_CMA] = TokenType.Comma;
const staticUnitTable: { [code: number]: TokenType; } = {};
staticUnitTable['em'] = TokenType.EMS;
staticUnitTable['ex'] = TokenType.EXS;
staticUnitTable['px'] = TokenType.Length;
staticUnitTable['cm'] = TokenType.Length;
staticUnitTable['mm'] = TokenType.Length;
staticUnitTable['in'] = TokenType.Length;
staticUnitTable['pt'] = TokenType.Length;
staticUnitTable['pc'] = TokenType.Length;
staticUnitTable['deg'] = TokenType.Angle;
staticUnitTable['rad'] = TokenType.Angle;
staticUnitTable['grad'] = TokenType.Angle;
staticUnitTable['ms'] = TokenType.Time;
staticUnitTable['s'] = TokenType.Time;
staticUnitTable['hz'] = TokenType.Freq;
staticUnitTable['khz'] = TokenType.Freq;
staticUnitTable['%'] = TokenType.Percentage;
staticUnitTable['dpi'] = TokenType.Resolution;
staticUnitTable['dpcm'] = TokenType.Resolution;
export class Scanner {
public stream: MultiLineStream;
public ignoreComment = true;
public ignoreWhitespace = true;
public setSource(input: string): void {
this.stream = new MultiLineStream(input);
}
public finishToken(offset: number, type: TokenType, text?: string): IToken {
return {
offset: offset,
len: this.stream.pos() - offset,
type: type,
text: text || this.stream.substring(offset)
};
}
public substring(offset: number, len: number): string {
return this.stream.substring(offset, offset + len);
}
public pos(): number {
return this.stream.pos();
}
public goBackTo(pos: number): void {
this.stream.goBackTo(pos);
}
public scan(): IToken {
// processes all whitespaces and comments
let triviaToken = this.trivia();
if (triviaToken !== null) {
return triviaToken;
}
let offset = this.stream.pos();
// End of file/input
if (this.stream.eos()) {
return this.finishToken(offset, TokenType.EOF);
}
// CDO <!--
if (this.stream.advanceIfChars([_LAN, _BNG, _MIN, _MIN])) {
return this.finishToken(offset, TokenType.CDO);
}
// CDC -->
if (this.stream.advanceIfChars([_MIN, _MIN, _RAN])) {
return this.finishToken(offset, TokenType.CDC);
}
// URL
let tokenType = this._url();
if (tokenType !== null) {
return this.finishToken(offset, tokenType);
}
let content: string[] = [];
if (this.ident(content)) {
return this.finishToken(offset, TokenType.Ident, content.join(''));
}
// at-keyword
if (this.stream.advanceIfChar(_ATS)) {
content = ['@'];
if (this._name(content)) {
let keywordText = content.join('');
if (keywordText === '@charset') {
return this.finishToken(offset, TokenType.Charset, keywordText);
}
return this.finishToken(offset, TokenType.AtKeyword, keywordText);
} else {
return this.finishToken(offset, TokenType.Delim);
}
}
// hash
if (this.stream.advanceIfChar(_HSH)) {
content = ['#'];
if (this._name(content)) {
return this.finishToken(offset, TokenType.Hash, content.join(''));
} else {
return this.finishToken(offset, TokenType.Delim);
}
}
// Important
if (this.stream.advanceIfChar(_BNG)) {
return this.finishToken(offset, TokenType.Exclamation);
}
// Numbers
if (this._number()) {
let pos = this.stream.pos();
content = [this.stream.substring(offset, pos)];
if (this.stream.advanceIfChar(_PRC)) {
// Percentage 43%
return this.finishToken(offset, TokenType.Percentage);
} else if (this.ident(content)) {
let dim = this.stream.substring(pos).toLowerCase();
tokenType = <TokenType>staticUnitTable[dim];
if (typeof tokenType !== 'undefined') {
// Known dimension 43px
return this.finishToken(offset, tokenType, content.join(''));
} else {
// Unknown dimension 43ft
return this.finishToken(offset, TokenType.Dimension, content.join(''));
}
}
return this.finishToken(offset, TokenType.Num);
}
// String, BadString
content = [];
tokenType = this._string(content);
if (tokenType !== null) {
return this.finishToken(offset, tokenType, content.join(''));
}
// single character tokens
tokenType = <TokenType>staticTokenTable[this.stream.peekChar()];
if (typeof tokenType !== 'undefined') {
this.stream.advance(1);
return this.finishToken(offset, tokenType);
}
// includes ~=
if (this.stream.peekChar(0) === _TLD && this.stream.peekChar(1) === _EQS) {
this.stream.advance(2);
return this.finishToken(offset, TokenType.Includes);
}
// DashMatch |=
if (this.stream.peekChar(0) === _PIP && this.stream.peekChar(1) === _EQS) {
this.stream.advance(2);
return this.finishToken(offset, TokenType.Dashmatch);
}
// Substring operator *=
if (this.stream.peekChar(0) === _MUL && this.stream.peekChar(1) === _EQS) {
this.stream.advance(2);
return this.finishToken(offset, TokenType.SubstringOperator);
}
// Substring operator ^=
if (this.stream.peekChar(0) === _HAT && this.stream.peekChar(1) === _EQS) {
this.stream.advance(2);
return this.finishToken(offset, TokenType.PrefixOperator);
}
// Substring operator $=
if (this.stream.peekChar(0) === _DLR && this.stream.peekChar(1) === _EQS) {
this.stream.advance(2);
return this.finishToken(offset, TokenType.SuffixOperator);
}
// Delim
this.stream.nextChar();
return this.finishToken(offset, TokenType.Delim);
}
private _matchWordAnyCase(characters: number[]): boolean {
let index = 0;
this.stream.advanceWhileChar((ch: number) => {
let result = characters[index] === ch || characters[index + 1] === ch;
if (result) {
index += 2;
}
return result;
});
if (index === characters.length) {
return true;
} else {
this.stream.goBack(index / 2);
return false;
}
}
protected trivia(): IToken {
while (true) {
let offset = this.stream.pos();
if (this._whitespace()) {
if (!this.ignoreWhitespace) {
return this.finishToken(offset, TokenType.Whitespace);
}
} else if (this.comment()) {
if (!this.ignoreComment) {
return this.finishToken(offset, TokenType.Comment);
}
} else {
return null;
}
}
}
protected comment(): boolean {
if (this.stream.advanceIfChars([_FSL, _MUL])) {
let success = false, hot = false;
this.stream.advanceWhileChar((ch) => {
if (hot && ch === _FSL) {
success = true;
return false;
}
hot = ch === _MUL;
return true;
});
if (success) {
this.stream.advance(1);
}
return true;
}
return false;
}
private _number(): boolean {
let npeek = 0, ch: number;
if (this.stream.peekChar() === _DOT) {
npeek = 1;
}
ch = this.stream.peekChar(npeek);
if (ch >= _0 && ch <= _9) {
this.stream.advance(npeek + 1);
this.stream.advanceWhileChar((ch) => {
return ch >= _0 && ch <= _9 || npeek === 0 && ch === _DOT;
});
return true;
}
return false;
}
private _newline(result: string[]): boolean {
let ch = this.stream.peekChar();
switch (ch) {
case _CAR:
case _LFD:
case _NWL:
this.stream.advance(1);
result.push(String.fromCharCode(ch));
if (ch === _CAR && this.stream.advanceIfChar(_NWL)) {
result.push('\n');
}
return true;
}
return false;
}
private _escape(result: string[], includeNewLines?: boolean): boolean {
let ch = this.stream.peekChar();
if (ch === _BSL) {
this.stream.advance(1);
ch = this.stream.peekChar();
let hexNumCount = 0;
while (hexNumCount < 6 && (ch >= _0 && ch <= _9 || ch >= _a && ch <= _f || ch >= _A && ch <= _F)) {
this.stream.advance(1);
ch = this.stream.peekChar();
hexNumCount++;
}
if (hexNumCount > 0) {
try {
let hexVal = parseInt(this.stream.substring(this.stream.pos() - hexNumCount), 16);
if (hexVal) {
result.push(String.fromCharCode(hexVal));
}
} catch (e) {
// ignore
}
// optional whitespace or new line, not part of result text
if (ch === _WSP || ch === _TAB) {
this.stream.advance(1);
} else {
this._newline([]);
}
return true;
}
if (ch !== _CAR && ch !== _LFD && ch !== _NWL) {
this.stream.advance(1);
result.push(String.fromCharCode(ch));
return true;
} else if (includeNewLines) {
return this._newline(result);
}
}
return false;
}
private _stringChar(closeQuote: number, result: string[]) {
// not closeQuote, not backslash, not newline
let ch = this.stream.peekChar();
if (ch !== 0 && ch !== closeQuote && ch !== _BSL && ch !== _CAR && ch !== _LFD && ch !== _NWL) {
this.stream.advance(1);
result.push(String.fromCharCode(ch));
return true;
}
return false;
};
private _string(result: string[]): TokenType {
if (this.stream.peekChar() === _SQO || this.stream.peekChar() === _DQO) {
let closeQuote = this.stream.nextChar();
result.push(String.fromCharCode(closeQuote));
while (this._stringChar(closeQuote, result) || this._escape(result, true)) {
// loop
}
if (this.stream.peekChar() === closeQuote) {
this.stream.nextChar();
result.push(String.fromCharCode(closeQuote));
return TokenType.String;
} else {
return TokenType.BadString;
}
}
return null;
}
private _url(): TokenType {
if (this._matchWordAnyCase(_url) || this._matchWordAnyCase(_url_prefix)) {
this._whitespace();
let tokenType = TokenType.URI, stringType = this._string([]);
if (stringType === TokenType.BadString) {
tokenType = TokenType.BadUri;
} else if (stringType === null) {
this.stream.advanceWhileChar((ch) => {
return ch !== _RPA;
});
tokenType = TokenType.URI;
}
this._whitespace();
if (this.stream.advanceIfChar(_RPA)) {
return tokenType;
} else {
return TokenType.BadUri;
}
}
return null;
}
private _whitespace(): boolean {
let n = this.stream.advanceWhileChar((ch) => {
return ch === _WSP || ch === _TAB || ch === _NWL || ch === _LFD || ch === _CAR;
});
return n > 0;
}
private _name(result: string[]): boolean {
let matched = false;
while (this._identChar(result) || this._escape(result)) {
matched = true;
}
return matched;
}
protected ident(result: string[]): boolean {
let pos = this.stream.pos();
let hasMinus = this._minus(result);
if (hasMinus && this._minus(result) /* -- */) {
let hasContent = false;
while (this._identChar(result) || this._escape(result)) {
hasContent = true;
}
if (hasContent) {
return true;
}
} else if (this._identFirstChar(result) || this._escape(result)) {
while (this._identChar(result) || this._escape(result)) {
// loop
}
return true;
}
this.stream.goBackTo(pos);
return false;
}
private _identFirstChar(result: string[]): boolean {
let ch = this.stream.peekChar();
if (ch === _USC || // _
ch >= _a && ch <= _z || // a-z
ch >= _A && ch <= _Z || // A-Z
ch >= 0x80 && ch <= 0xFFFF) { // nonascii
this.stream.advance(1);
result.push(String.fromCharCode(ch));
return true;
}
return false;
}
private _minus(result: string[]): boolean {
let ch = this.stream.peekChar();
if (ch === _MIN) {
this.stream.advance(1);
result.push(String.fromCharCode(ch));
return true;
}
return false;
}
private _identChar(result: string[]): boolean {
let ch = this.stream.peekChar();
if (ch === _USC || // _
ch === _MIN || // -
ch >= _a && ch <= _z || // a-z
ch >= _A && ch <= _Z || // A-Z
ch >= _0 && ch <= _9 || // 0/9
ch >= 0x80 && ch <= 0xFFFF) { // nonascii
this.stream.advance(1);
result.push(String.fromCharCode(ch));
return true;
}
return false;
}
}
/*---------------------------------------------------------------------------------------------
* 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 {findFirst} from '../utils/arrays';
export class Scope {
public parent: Scope;
public children: Scope[];
public offset: number;
public length: number;
private symbols: Symbol[];
constructor(offset: number, length: number) {
this.offset = offset;
this.length = length;
this.symbols = [];
this.parent = null;
this.children = [];
}
public addChild(scope: Scope): void {
this.children.push(scope);
scope.setParent(this);
}
public setParent(scope: Scope): void {
this.parent = scope;
}
public findScope(offset: number, length: number = 0): Scope {
if (this.offset <= offset && this.offset + this.length > offset + length || this.offset === offset && this.length === length) {
return this.findInScope(offset, length);
}
return null;
}
private findInScope(offset: number, length: number = 0): Scope {
// find the first scope child that has an offset larger than offset + length
let end = offset + length;
let idx = findFirst(this.children, s => s.offset > end);
if (idx === 0) {
// all scopes have offsets larger than our end
return this;
}
let res = this.children[idx - 1];
if (res.offset <= offset && res.offset + res.length >= offset + length) {
return res.findInScope(offset, length);
}
return this;
}
public addSymbol(symbol: Symbol): void {
this.symbols.push(symbol);
}
public getSymbol(name: string, type: nodes.ReferenceType): Symbol {
for (let index = 0; index < this.symbols.length; index++) {
let symbol = this.symbols[index];
if (symbol.name === name && symbol.type === type) {
return symbol;
}
}
return null;
}
public getSymbols(): Symbol[] {
return this.symbols;
}
}
export class GlobalScope extends Scope {
constructor() {
super(0, Number.MAX_VALUE);
}
}
export class Symbol {
public name: string;
public type: nodes.ReferenceType;
public node: nodes.Node;
constructor(name: string, node: nodes.Node, type: nodes.ReferenceType) {
this.name = name;
this.node = node;
this.type = type;
}
}
export class ScopeBuilder implements nodes.IVisitor {
public scope: Scope;
constructor(scope: Scope) {
this.scope = scope;
}
private addSymbol(node: nodes.Node, name: string, type: nodes.ReferenceType): void {
if (node.offset !== -1) {
let current = this.scope.findScope(node.offset, node.length);
current.addSymbol(new Symbol(name, node, type));
}
}
private addScope(node: nodes.Node): Scope {
if (node.offset !== -1) {
let current = this.scope.findScope(node.offset, node.length);
if (current.offset !== node.offset || current.length !== node.length) { // scope already known?
let newScope = new Scope(node.offset, node.length);
current.addChild(newScope);
return newScope;
}
return current;
}
return null;
}
private addSymbolToChildScope(scopeNode: nodes.Node, node: nodes.Node, name: string, type: nodes.ReferenceType): void {
if (scopeNode && scopeNode.offset !== -1) {
let current = this.addScope(scopeNode); // create the scope or gets the existing one
current.addSymbol(new Symbol(name, node, type));
}
}
public visitNode(node: nodes.Node): boolean {
switch (node.type) {
case nodes.NodeType.Keyframe:
this.addSymbol(node, (<nodes.Keyframe>node).getName(), nodes.ReferenceType.Keyframe);
return true;
case nodes.NodeType.Declaration:
return this.visitDeclarationNode(<nodes.Declaration>node);
case nodes.NodeType.VariableDeclaration:
return this.visitVariableDeclarationNode(<nodes.VariableDeclaration>node);
case nodes.NodeType.Ruleset:
return this.visitRuleSet(<nodes.RuleSet>node);
case nodes.NodeType.MixinDeclaration:
this.addSymbol(node, (<nodes.MixinDeclaration>node).getName(), nodes.ReferenceType.Mixin);
return true;
case nodes.NodeType.FunctionDeclaration:
this.addSymbol(node, (<nodes.FunctionDeclaration>node).getName(), nodes.ReferenceType.Function);
return true;
case nodes.NodeType.FunctionParameter: {
// parameters are part of the body scope
let scopeNode = (<nodes.BodyDeclaration>node.getParent()).getDeclarations();
if (scopeNode) {
this.addSymbolToChildScope(scopeNode, node, (<nodes.FunctionParameter>node).getName(), nodes.ReferenceType.Variable);
}
return true;
}
case nodes.NodeType.Declarations:
this.addScope(node);
return true;
case nodes.NodeType.For:
case nodes.NodeType.Each: {
let forOrEachNode = <nodes.ForStatement | nodes.EachStatement>node;
let scopeNode = forOrEachNode.getDeclarations();
if (scopeNode) {
this.addSymbolToChildScope(scopeNode, forOrEachNode.variable, forOrEachNode.variable.getName(), nodes.ReferenceType.Variable);
}
return true;
}
}
return true;
}
public visitRuleSet(node: nodes.RuleSet): boolean {
let current = this.scope.findScope(node.offset, node.length);
node.getSelectors().getChildren().forEach((child) => {
if (child instanceof nodes.Selector) {
if (child.getChildren().length === 1) { // only selectors with a single element can be extended
current.addSymbol(new Symbol(child.getChild(0).getText(), child, nodes.ReferenceType.Rule));
}
}
});
return true;
}
public visitVariableDeclarationNode(node: nodes.VariableDeclaration): boolean {
this.addSymbol(node, (<nodes.VariableDeclaration>node).getName(), nodes.ReferenceType.Variable);
return true;
}
public visitDeclarationNode(node: nodes.Declaration): boolean {
if (Symbols.isCssVariable(node.getProperty().getIdentifier())) {
this.addCSSVariable(node.getProperty(), node.getProperty().getName(), nodes.ReferenceType.Variable);
}
return true;
}
private addCSSVariable(node: nodes.Node, name: string, type: nodes.ReferenceType): void {
if (node.offset !== -1) {
let globalScope = this.getGlobalScope(node, name, type);
globalScope.addSymbol(new Symbol(name, node, type));
}
}
private getGlobalScope(node: nodes.Node, name: string, type: nodes.ReferenceType): Scope {
let current = this.scope.findScope(node.offset, node.length);
while (current.parent !== null) {
current = current.parent;
}
return current;
}
}
export class Symbols {
private global: Scope;
constructor(node: nodes.Node) {
this.global = new GlobalScope();
node.accept(new ScopeBuilder(this.global));
}
public findSymbolsAtOffset(offset: number, referenceType: nodes.ReferenceType): Symbol[] {
let scope = this.global.findScope(offset, 0);
let result: Symbol[] = [];
let names: { [name: string]: boolean } = {};
while (scope) {
let symbols = scope.getSymbols();
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];
if (symbol.node.offset <= offset && symbol.type === referenceType && !names[symbol.name]) {
result.push(symbol);
names[symbol.name] = true;
}
}
scope = scope.parent;
}
return result;
}
private internalFindSymbol(node: nodes.Node, referenceTypes: nodes.ReferenceType[]): Symbol {
let scopeNode = node;
if (node.parent instanceof nodes.FunctionParameter && node.parent.getParent() instanceof nodes.BodyDeclaration) {
scopeNode = (<nodes.BodyDeclaration>node.parent.getParent()).getDeclarations();
}
if (node.parent instanceof nodes.FunctionArgument && node.parent.getParent() instanceof nodes.Function) {
let funcId = (<nodes.Function>node.parent.getParent()).getIdentifier();
if (funcId) {
let functionSymbol = this.internalFindSymbol(funcId, [nodes.ReferenceType.Function]);
if (functionSymbol) {
scopeNode = (<nodes.FunctionDeclaration>functionSymbol.node).getDeclarations();
}
}
}
if (!scopeNode) {
return null;
}
let name = node.getText();
let scope = this.global.findScope(scopeNode.offset, scopeNode.length);
while (scope) {
for (let index = 0; index < referenceTypes.length; index++) {
let type = referenceTypes[index];
let symbol = scope.getSymbol(name, type);
if (symbol) {
return symbol;
}
}
scope = scope.parent;
}
return null;
}
private evaluateReferenceTypes(node: nodes.Node): nodes.ReferenceType[] {
if (node instanceof nodes.Identifier) {
let referenceTypes = (<nodes.Identifier>node).referenceTypes;
if (referenceTypes) {
return referenceTypes;
} else {
if (Symbols.isCssVariable(node)) {
return [nodes.ReferenceType.Variable];
}
// are a reference to a keyframe?
let decl = nodes.getParentDeclaration(node);
if (decl) {
let propertyName = decl.getNonPrefixedPropertyName();
if ((propertyName === 'animation' || propertyName === 'animation-name')
&& decl.getValue() && decl.getValue().offset === node.offset) {
return [nodes.ReferenceType.Keyframe];
}
}
}
} else if (node instanceof nodes.Variable) {
return [nodes.ReferenceType.Variable];
}
let selector = node.findParent(nodes.NodeType.Selector);
if (selector) {
return [nodes.ReferenceType.Rule];
}
let extendsRef = <nodes.ExtendsReference>node.findParent(nodes.NodeType.ExtendsReference);
if (extendsRef) {
return [nodes.ReferenceType.Rule];
}
return null;
}
public findSymbolFromNode(node: nodes.Node): Symbol {
if (!node) {
return null;
}
while (node.type === nodes.NodeType.Interpolation) {
node = node.getParent();
}
let referenceTypes = this.evaluateReferenceTypes(node);
if (referenceTypes) {
return this.internalFindSymbol(node, referenceTypes);
}
return null;
}
public matchesSymbol(node: nodes.Node, symbol: Symbol): boolean {
if (!node) {
return null;
}
while (node.type === nodes.NodeType.Interpolation) {
node = node.getParent();
}
if (symbol.name.length !== node.length || symbol.name !== node.getText()) {
return false;
}
let referenceTypes = this.evaluateReferenceTypes(node);
if (!referenceTypes || referenceTypes.indexOf(symbol.type) === -1) {
return false;
}
let nodeSymbol = this.internalFindSymbol(node, referenceTypes);
return nodeSymbol === symbol;
}
public findSymbol(name: string, type: nodes.ReferenceType, offset: number): Symbol {
let scope = this.global.findScope(offset);
while (scope) {
let symbol = scope.getSymbol(name, type);
if (symbol) {
return symbol;
}
scope = scope.parent;
}
return null;
}
public static isCssVariable(identifier: nodes.Identifier): boolean {
return /^--/.test(identifier.getText());
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import * as languageFacts from './languageFacts';
import {difference} from '../utils/strings';
import {Rules} from '../services/lintRules';
import {TextDocument, Range, CodeActionContext, Diagnostic, Command, TextEdit} from 'vscode-languageserver';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class CSSCodeActions {
constructor() {
}
public doCodeActions(document: TextDocument, range: Range, context: CodeActionContext, stylesheet: nodes.Stylesheet): Thenable<Command[]> {
let result: Command[] = [];
if (context.diagnostics) {
for (let diagnostic of context.diagnostics) {
this.appendFixesForMarker(document, stylesheet, diagnostic, result);
}
}
return Promise.resolve(result);
}
private getFixesForUnknownProperty(document: TextDocument, property: nodes.Property, marker: Diagnostic, result: Command[]): void {
interface RankedProperty {
property: string;
score: number;
}
let propertyName = property.getName();
let candidates: RankedProperty[] = [];
for (let p in languageFacts.getProperties()) {
let score = difference(propertyName, p);
if (score >= propertyName.length / 2 /*score_lim*/) {
candidates.push({ property: p, score });
}
}
// Sort in descending order.
candidates.sort((a, b) => {
return b.score - a.score;
});
let maxActions = 3;
for (let candidate of candidates) {
let propertyName = candidate.property;
let title = localize('css.codeaction.rename', "Rename to '{0}'", propertyName);
let edit = TextEdit.replace(marker.range, propertyName);
result.push(Command.create(title, '_css.applyCodeAction', document.uri, document.version, [edit]));
if (--maxActions <= 0) {
return;
}
}
}
private appendFixesForMarker(document: TextDocument, stylesheet: nodes.Stylesheet, marker: Diagnostic, result: Command[]): void {
if (marker.code !== Rules.UnknownProperty.id) {
return;
}
let offset = document.offsetAt(marker.range.start);
let end = document.offsetAt(marker.range.end);
let nodepath = nodes.getNodePath(stylesheet, offset);
for (let i = nodepath.length - 1; i >= 0; i--) {
let node = nodepath[i];
if (node instanceof nodes.Declaration) {
let property = (<nodes.Declaration>node).getProperty();
if (property && property.offset === offset && property.end === end) {
this.getFixesForUnknownProperty(document, property, marker, result);
return;
}
}
}
}
}
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import {Symbols} from '../parser/cssSymbolScope';
import * as languageFacts from './languageFacts';
import * as strings from '../utils/strings';
import {TextDocument, Position, CompletionList, CompletionItemKind} from 'vscode-languageserver';
export class CSSCompletion {
variablePrefix: string;
position: Position;
offset: number;
currentWord: string;
textDocument: TextDocument;
styleSheet: nodes.Stylesheet;
symbolContext: Symbols;
constructor(variablePrefix: string = null) {
this.variablePrefix = variablePrefix;
}
private getSymbolContext(): Symbols {
if (!this.symbolContext) {
this.symbolContext = new Symbols(this.styleSheet);
}
return this.symbolContext;
}
public doComplete(document: TextDocument, position: Position, styleSheet: nodes.Stylesheet): CompletionList {
this.offset = document.offsetAt(position);
this.position = position;
this.currentWord = getCurrentWord(document, this.offset);
this.textDocument = document;
this.styleSheet = styleSheet;
let result: CompletionList = { isIncomplete: false, items: [] };
let nodepath = nodes.getNodePath(this.styleSheet, this.offset);
for (let i = nodepath.length - 1; i >= 0; i--) {
let node = nodepath[i];
if (node instanceof nodes.Property) {
this.getCompletionsForDeclarationProperty(result);
} else if (node instanceof nodes.Expression) {
this.getCompletionsForExpression(<nodes.Expression>node, result);
} else if (node instanceof nodes.SimpleSelector) {
let parentRuleSet = <nodes.RuleSet>node.findParent(nodes.NodeType.Ruleset);
this.getCompletionsForSelector(parentRuleSet, result);
} else if (node instanceof nodes.Declarations) {
this.getCompletionsForDeclarations(<nodes.Declarations>node, result);
} else if (node instanceof nodes.VariableDeclaration) {
this.getCompletionsForVariableDeclaration(<nodes.VariableDeclaration>node, result);
} else if (node instanceof nodes.RuleSet) {
this.getCompletionsForRuleSet(<nodes.RuleSet>node, result);
} else if (node instanceof nodes.Interpolation) {
this.getCompletionsForInterpolation(<nodes.Interpolation>node, result);
} else if (node instanceof nodes.FunctionArgument) {
this.getCompletionsForFunctionArgument(<nodes.FunctionArgument>node, <nodes.Function>node.getParent(), result);
} else if (node instanceof nodes.FunctionDeclaration) {
this.getCompletionsForFunctionDeclaration(<nodes.FunctionDeclaration>node, result);
} else if (node instanceof nodes.Function) {
this.getCompletionsForFunctionArgument(null, <nodes.Function>node, result);
}
if (result.items.length > 0) {
return result;
}
}
this.getCompletionsForStylesheet(result);
if (result.items.length > 0) {
return result;
}
if (this.variablePrefix && this.currentWord.indexOf(this.variablePrefix) === 0) {
this.getVariableProposals(result);
if (result.items.length > 0) {
return result;
}
}
// no match, don't show text matches
return result;
}
public getCompletionsForDeclarationProperty(result: CompletionList): CompletionList {
return this.getPropertyProposals(result);
}
private getPropertyProposals(result: CompletionList): CompletionList {
let properties = languageFacts.getProperties();
for (let key in properties) {
if (properties.hasOwnProperty(key)) {
let entry = properties[key];
if (entry.browsers.onCodeComplete) {
result.items.push({
label: entry.name,
documentation: languageFacts.getEntryDescription(entry),
insertText: entry.name + ': ',
kind: CompletionItemKind.Property
});
}
}
}
return result;
}
public getCompletionsForDeclarationValue(node: nodes.Declaration, result: CompletionList): CompletionList {
let propertyName = node.getFullPropertyName();
let entry = languageFacts.getProperties()[propertyName];
if (entry) {
this.getColorProposals(entry, result);
this.getPositionProposals(entry, result);
this.getRepeatStyleProposals(entry, result);
this.getLineProposals(entry, result);
this.getBoxProposals(entry, result);
this.getImageProposals(entry, result);
this.getTimingFunctionProposals(entry, result);
this.getBasicShapeProposals(entry, result);
this.getValueEnumProposals(entry, result);
this.getCSSWideKeywordProposals(entry, result);
this.getUnitProposals(entry, result);
} else {
let existingValues = new Set();
this.styleSheet.accept(new ValuesCollector(propertyName, existingValues));
existingValues.getEntries().forEach((existingValue) => {
result.items.push({
label: existingValue,
insertText: existingValue,
kind: CompletionItemKind.Value
});
});
}
this.getVariableProposals(result);
this.getTermProposals(result);
return result;
}
public getValueEnumProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.values) {
entry.values.forEach((value) => {
if (languageFacts.isCommonValue(value)) { // only show if supported by more than one browser
result.items.push({
label: value.name,
documentation: languageFacts.getEntryDescription(value),
insertText: value.name,
kind: CompletionItemKind.Value
});
}
});
}
return result;
}
public getCSSWideKeywordProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
for (let keywords in languageFacts.cssWideKeywords) {
result.items.push({
label: keywords,
documentation: languageFacts.cssWideKeywords[keywords],
insertText: keywords,
kind: CompletionItemKind.Value
});
}
return result;
}
public getCompletionsForInterpolation(node: nodes.Interpolation, result: CompletionList): CompletionList {
if (this.offset >= node.offset + 2) {
this.getVariableProposals(result);
}
return result;
}
public getVariableProposals(result: CompletionList): CompletionList {
let symbols = this.getSymbolContext().findSymbolsAtOffset(this.offset, nodes.ReferenceType.Variable);
symbols.forEach((symbol) => {
result.items.push({
label: symbol.name,
insertText: strings.startsWith(symbol.name, '--') ? `let(${symbol.name})` : symbol.name,
kind: CompletionItemKind.Variable
});
});
return result;
}
public getVariableProposalsForCSSVarFunction(result: CompletionList): CompletionList {
let symbols = this.getSymbolContext().findSymbolsAtOffset(this.offset, nodes.ReferenceType.Variable);
symbols = symbols.filter((symbol): boolean => {
return strings.startsWith(symbol.name, '--');
});
symbols.forEach((symbol) => {
result.items.push({
label: symbol.name,
insertText: symbol.name,
kind: CompletionItemKind.Variable
});
});
return result;
}
public getUnitProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
let currentWord = '0';
if (this.currentWord.length > 0) {
let numMatch = this.currentWord.match(/-?\d[\.\d+]*/);
if (numMatch) {
currentWord = numMatch[0];
}
}
entry.restrictions.forEach((restriction) => {
let units = languageFacts.units[restriction];
if (units) {
units.forEach(function (unit: string) {
result.items.push({
label: currentWord + unit,
insertText: currentWord + unit,
kind: CompletionItemKind.Unit
});
});
}
});
result.isIncomplete = true;
return result;
}
protected getColorProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('color') !== -1) {
for (let color in languageFacts.colors) {
result.items.push({
label: color,
documentation: languageFacts.colors[color],
insertText: color,
kind: CompletionItemKind.Color
});
}
for (let color in languageFacts.colorKeywords) {
result.items.push({
label: color,
documentation: languageFacts.colorKeywords[color],
insertText: color,
kind: CompletionItemKind.Value
});
}
let colorValues = new Set();
this.styleSheet.accept(new ColorValueCollector(colorValues));
colorValues.getEntries().forEach((color) => {
result.items.push({
label: color,
insertText: color,
kind: CompletionItemKind.Color
});
});
languageFacts.colorFunctions.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, '{{$1}}'),
kind: CompletionItemKind.Function
});
});
}
return result;
}
protected getPositionProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('position') !== -1) {
for (let position in languageFacts.positionKeywords) {
result.items.push({
label: position,
documentation: languageFacts.positionKeywords[position],
insertText: position,
kind: CompletionItemKind.Value
});
}
}
return result;
}
protected getRepeatStyleProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('repeat') !== -1) {
for (let repeat in languageFacts.repeatStyleKeywords) {
result.items.push({
label: repeat,
documentation: languageFacts.repeatStyleKeywords[repeat],
insertText: repeat,
kind: CompletionItemKind.Value
});
}
}
return result;
}
protected getLineProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('line-style') !== -1) {
for (let lineStyle in languageFacts.lineStyleKeywords) {
result.items.push({
label: lineStyle,
documentation: languageFacts.lineStyleKeywords[lineStyle],
insertText: lineStyle,
kind: CompletionItemKind.Value
});
}
}
if (entry.restrictions.indexOf('line-width') !== -1) {
languageFacts.lineWidthKeywords.forEach((lineWidth) => {
result.items.push({
label: lineWidth,
insertText: lineWidth,
kind: CompletionItemKind.Value
});
});
}
return result;
}
protected getBoxProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
let geometryBox = entry.restrictions.indexOf('geometry-box');
if (geometryBox !== -1) {
for (let box in languageFacts.geometryBoxKeywords) {
result.items.push({
label: box,
documentation: languageFacts.geometryBoxKeywords[box],
insertText: box,
kind: CompletionItemKind.Value
});
}
}
if (entry.restrictions.indexOf('box') !== -1 || geometryBox !== -1) {
for (let box in languageFacts.boxKeywords) {
result.items.push({
label: box,
documentation: languageFacts.boxKeywords[box],
insertText: box,
kind: CompletionItemKind.Value
});
}
}
return result;
}
protected getImageProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('image') !== -1) {
for (let image in languageFacts.imageFunctions) {
result.items.push({
label: image,
documentation: languageFacts.imageFunctions[image],
insertText: image,
kind: CompletionItemKind.Function
});
}
}
return result;
}
protected getTimingFunctionProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('timing-function') !== -1) {
for (let timing in languageFacts.transitionTimingFunctions) {
result.items.push({
label: timing,
documentation: languageFacts.transitionTimingFunctions[timing],
insertText: timing,
kind: CompletionItemKind.Function
});
}
}
return result;
}
protected getBasicShapeProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList {
if (entry.restrictions.indexOf('shape') !== -1) {
for (let shape in languageFacts.basicShapeFunctions) {
result.items.push({
label: shape,
documentation: languageFacts.basicShapeFunctions[shape],
insertText: shape,
kind: CompletionItemKind.Function
});
}
}
return result;
}
public getCompletionsForStylesheet(result: CompletionList): CompletionList {
let node = this.styleSheet.findFirstChildBeforeOffset(this.offset);
if (!node) {
return this.getCompletionForTopLevel(result);
}
if (node instanceof nodes.RuleSet) {
return this.getCompletionsForRuleSet(<nodes.RuleSet>node, result);
}
return result;
}
public getCompletionForTopLevel(result: CompletionList): CompletionList {
languageFacts.getAtDirectives().forEach(function (entry) {
if (entry.browsers.count > 0) {
result.items.push({
label: entry.name,
insertText: entry.name,
documentation: languageFacts.getEntryDescription(entry),
kind: CompletionItemKind.Keyword
});
}
});
this.getCompletionsForSelector(null, result);
return result;
}
public getCompletionsForRuleSet(ruleSet: nodes.RuleSet, result: CompletionList): CompletionList {
let declarations = ruleSet.getDeclarations();
let isAfter = declarations && declarations.endsWith('}') && this.offset >= declarations.end;
if (isAfter) {
return this.getCompletionForTopLevel(result);
}
let isInSelectors = !declarations || this.offset <= declarations.offset;
if (isInSelectors) {
return this.getCompletionsForSelector(ruleSet, result);
}
ruleSet.findParent(nodes.NodeType.Ruleset);
return this.getCompletionsForDeclarations(ruleSet.getDeclarations(), result);
}
public getCompletionsForSelector(ruleSet: nodes.RuleSet, result: CompletionList): CompletionList {
languageFacts.getPseudoClasses().forEach((entry) => {
if (entry.browsers.onCodeComplete) {
result.items.push({
label: entry.name,
insertText: entry.name,
documentation: languageFacts.getEntryDescription(entry),
kind: CompletionItemKind.Function
});
}
});
languageFacts.getPseudoElements().forEach((entry) => {
if (entry.browsers.onCodeComplete) {
result.items.push({
label: entry.name,
insertText: entry.name,
documentation: languageFacts.getEntryDescription(entry),
kind: CompletionItemKind.Function
});
}
});
languageFacts.html5Tags.forEach((entry) => {
result.items.push({
label: entry,
insertText: entry,
kind: CompletionItemKind.Keyword
});
});
languageFacts.svgElements.forEach((entry) => {
result.items.push({
label: entry,
insertText: entry,
kind: CompletionItemKind.Keyword
});
});
let visited: { [name: string]: boolean } = {};
visited[this.currentWord] = true;
let textProvider = this.styleSheet.getTextProvider();
this.styleSheet.accept(n => {
if (n.type === nodes.NodeType.SimpleSelector && n.length > 0) {
let selector = textProvider(n.offset, n.length);
if (selector.charAt(0) === '.' && !visited[selector]) {
visited[selector] = true;
result.items.push({
label: selector,
insertText: selector,
kind: CompletionItemKind.Keyword
});
}
return false;
}
return true;
});
if (ruleSet && ruleSet.isNested()) {
let selector = ruleSet.getSelectors().findFirstChildBeforeOffset(this.offset);
if (selector && ruleSet.getSelectors().getChildren().indexOf(selector) === 0) {
this.getPropertyProposals(result);
}
}
return result;
}
public getCompletionsForDeclarations(declarations: nodes.Declarations, result: CompletionList): CompletionList {
if (!declarations) { // incomplete nodes
return result;
}
let node = declarations.findFirstChildBeforeOffset(this.offset);
if (!node) {
return this.getCompletionsForDeclarationProperty(result);
}
if (node instanceof nodes.AbstractDeclaration) {
let declaration = <nodes.AbstractDeclaration>node;
if ((!isDefined(declaration.colonPosition) || this.offset <= declaration.colonPosition) || (isDefined(declaration.semicolonPosition) && declaration.semicolonPosition < this.offset)) {
if (this.offset === declaration.semicolonPosition + 1) {
return result; // don't show new properties right after semicolon (see Bug 15421:[intellisense] [css] Be less aggressive when manually typing CSS)
}
// complete property
return this.getCompletionsForDeclarationProperty(result);
}
if (declaration instanceof nodes.Declaration) {
// complete value
return this.getCompletionsForDeclarationValue(declaration, result);
}
}
return result;
}
public getCompletionsForVariableDeclaration(declaration: nodes.VariableDeclaration, result: CompletionList): CompletionList {
if (this.offset > declaration.colonPosition) {
this.getVariableProposals(result);
}
return result;
}
public getCompletionsForExpression(expression: nodes.Expression, result: CompletionList): CompletionList {
if (expression.getParent() instanceof nodes.FunctionArgument) {
this.getCompletionsForFunctionArgument(<nodes.FunctionArgument>expression.getParent(), <nodes.Function>expression.getParent().getParent(), result);
return result;
}
let declaration = <nodes.Declaration>expression.findParent(nodes.NodeType.Declaration);
if (!declaration) {
this.getTermProposals(result);
return result;
}
let node = expression.findChildAtOffset(this.offset, true);
if (!node) {
return this.getCompletionsForDeclarationValue(declaration, result);
}
if (node instanceof nodes.NumericValue || node instanceof nodes.Identifier) {
return this.getCompletionsForDeclarationValue(declaration, result);
}
return result;
}
public getCompletionsForFunctionArgument(arg: nodes.FunctionArgument, func: nodes.Function, result: CompletionList): CompletionList {
if (func.getIdentifier().getText() === 'let') {
if (!func.getArguments().hasChildren() || func.getArguments().getChild(0) === arg) {
this.getVariableProposalsForCSSVarFunction(result);
}
}
return result;
}
public getCompletionsForFunctionDeclaration(decl: nodes.FunctionDeclaration, result: CompletionList): CompletionList {
let declarations = decl.getDeclarations();
if (declarations && this.offset > declarations.offset && this.offset < declarations.end) {
this.getTermProposals(result);
}
return result;
}
public getTermProposals(result: CompletionList): CompletionList {
let allFunctions = this.getSymbolContext().findSymbolsAtOffset(this.offset, nodes.ReferenceType.Function);
allFunctions.forEach((functionSymbol) => {
if (functionSymbol.node instanceof nodes.FunctionDeclaration) {
let functionDecl = <nodes.FunctionDeclaration>functionSymbol.node;
let params = functionDecl.getParameters().getChildren().map((c) => {
return (c instanceof nodes.FunctionParameter) ? (<nodes.FunctionParameter>c).getName() : c.getText();
});
result.items.push({
label: functionSymbol.name,
detail: functionSymbol.name + '(' + params.join(', ') + ')',
insertText: functionSymbol.name + '(' + params.map((p) => '{{' + p + '}}').join(', ') + ')',
kind: CompletionItemKind.Function
});
}
});
return result;
}
}
class Set {
private entries: { [key: string]: boolean } = {};
public add(entry: string): void {
this.entries[entry] = true;
}
public getEntries(): string[] {
return Object.keys(this.entries);
}
}
class InternalValueCollector implements nodes.IVisitor {
constructor(public entries: Set) {
// nothing to do
}
public visitNode(node: nodes.Node): boolean {
if (node instanceof nodes.Identifier || node instanceof nodes.NumericValue || node instanceof nodes.HexColorValue) {
this.entries.add(node.getText());
}
return true;
}
}
class ValuesCollector implements nodes.IVisitor {
constructor(public propertyName: string, public entries: Set) {
// nothing to do
}
private matchesProperty(decl: nodes.Declaration): boolean {
let propertyName = decl.getFullPropertyName();
return this.propertyName === propertyName;
}
public visitNode(node: nodes.Node): boolean {
if (node instanceof nodes.Declaration) {
if (this.matchesProperty(<nodes.Declaration>node)) {
let value = (<nodes.Declaration>node).getValue();
if (value) {
value.accept(new InternalValueCollector(this.entries));
}
}
}
return true;
}
}
class ColorValueCollector implements nodes.IVisitor {
constructor(public entries: Set) {
// nothing to do
}
public visitNode(node: nodes.Node): boolean {
if (node instanceof nodes.HexColorValue || (node instanceof nodes.Function && languageFacts.isColorConstructor(<nodes.Function>node))) {
this.entries.add(node.getText());
}
return true;
}
}
function isDefined(obj: any): boolean {
return typeof obj !== 'undefined';
}
function getCurrentWord(document: TextDocument, offset: number) {
let i = offset - 1;
let text = document.getText();
while (i >= 0 && ' \t\n\r":{[,'.indexOf(text.charAt(i)) === -1) {
i--;
}
return text.substring(i + 1, offset);
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import * as languageFacts from './languageFacts';
import {TextDocument, Range, Position, Hover} from 'vscode-languageserver';
import {selectorToMarkedString, simpleSelectorToMarkedString} from './selectorPrinting';
export class CSSHover {
constructor() {
}
public doHover(document: TextDocument, position: Position, stylesheet: nodes.Stylesheet): Thenable<Hover> {
function getRange(node: nodes.Node) {
return Range.create(document.positionAt(node.offset), document.positionAt(node.end));
}
let offset = document.offsetAt(position);
let nodepath = nodes.getNodePath(stylesheet, offset);
for (let i = 0; i < nodepath.length; i++) {
let node = nodepath[i];
if (node instanceof nodes.Selector) {
return Promise.resolve({
contents: selectorToMarkedString(<nodes.Selector>node),
range: getRange(node)
});
}
if (node instanceof nodes.SimpleSelector) {
return Promise.resolve({
contents: simpleSelectorToMarkedString(<nodes.SimpleSelector>node),
range: getRange(node)
});
}
if (node instanceof nodes.Declaration) {
let propertyName = node.getFullPropertyName();
let entry = languageFacts.getProperties()[propertyName];
if (entry) {
return Promise.resolve({
contents: entry.description,
range: getRange(node)
});
}
}
}
return null;
}
}
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import {TextDocument, Range, Position, Location, DocumentHighlightKind, DocumentHighlight, SymbolInformation, SymbolKind} from 'vscode-languageserver';
import {Symbols} from '../parser/cssSymbolScope';
import {isColorValue} from '../services/languageFacts';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class CSSNavigation {
public findDefinition(document: TextDocument, position: Position, stylesheet: nodes.Node): Location {
let symbols = new Symbols(stylesheet);
let offset = document.offsetAt(position);
let node = nodes.getNodeAtOffset(stylesheet, offset);
if (!node) {
return null;
}
let symbol = symbols.findSymbolFromNode(node);
if (!symbol) {
return null;
}
return {
uri: document.uri,
range: getRange(node, document)
};
}
public findReferences(document: TextDocument, position: Position, stylesheet: nodes.Node): Location[] {
return this.findDocumentHighlights(document, position, stylesheet).map(h => {
return {
uri: document.uri,
range: h.range
};
});
}
public findDocumentHighlights(document: TextDocument, position: Position, stylesheet: nodes.Node): DocumentHighlight[] {
let result: DocumentHighlight[] = [];
let offset = document.offsetAt(position);
let node = nodes.getNodeAtOffset(stylesheet, offset);
if (!node || node.type === nodes.NodeType.Stylesheet || node.type === nodes.NodeType.Declarations) {
return result;
}
let symbols = new Symbols(stylesheet);
let symbol = symbols.findSymbolFromNode(node);
let name = node.getText();
stylesheet.accept((candidate) => {
if (symbol) {
if (symbols.matchesSymbol(candidate, symbol)) {
result.push({
kind: getHighlightKind(candidate),
range: getRange(candidate, document)
});
return false;
}
} else if (node.type === candidate.type && node.length === candidate.length && name === candidate.getText()) {
// Same node type and data
result.push({
kind: getHighlightKind(candidate),
range: getRange(candidate, document)
});
}
return true;
});
return result;
}
public findDocumentSymbols(document: TextDocument, stylesheet: nodes.Stylesheet): SymbolInformation[] {
let result: SymbolInformation[] = [];
stylesheet.accept((node) => {
let entry: SymbolInformation = {
name: null,
kind: SymbolKind.Class, // TODO@Martin: find a good SymbolKind
location: null
};
if (node instanceof nodes.Selector) {
entry.name = node.getText();
} else if (node instanceof nodes.VariableDeclaration) {
entry.name = (<nodes.VariableDeclaration>node).getName();
entry.kind = SymbolKind.Variable;
} else if (node instanceof nodes.MixinDeclaration) {
entry.name = (<nodes.MixinDeclaration>node).getName();
entry.kind = SymbolKind.Method;
} else if (node instanceof nodes.FunctionDeclaration) {
entry.name = (<nodes.FunctionDeclaration>node).getName();
entry.kind = SymbolKind.Function;
} else if (node instanceof nodes.Keyframe) {
entry.name = localize('literal.keyframes', "@keyframes {0}", (<nodes.Keyframe>node).getName());
} else if (node instanceof nodes.FontFace) {
entry.name = localize('literal.fontface', "@font-face");
}
if (entry.name) {
entry.location = Location.create(document.uri, getRange(node, document));
result.push(entry);
}
return true;
});
return result;
}
public findColorSymbols(document: TextDocument, stylesheet: nodes.Stylesheet): Range[] {
let result: Range[] = [];
stylesheet.accept((node) => {
if (isColorValue(node)) {
result.push(getRange(node, document));
}
return true;
});
return result;
}
}
function getRange(node: nodes.Node, document: TextDocument) : Range {
return Range.create(document.positionAt(node.offset), document.positionAt(node.end));
}
function getHighlightKind(node: nodes.Node): DocumentHighlightKind {
if (node.type === nodes.NodeType.Selector) {
return DocumentHighlightKind.Write;
}
if (node instanceof nodes.Identifier) {
if (node.parent && node.parent instanceof nodes.Property) {
if (Symbols.isCssVariable(node)) {
return DocumentHighlightKind.Write;
}
}
}
if (node.parent) {
switch (node.parent.type) {
case nodes.NodeType.FunctionDeclaration:
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.Keyframe:
case nodes.NodeType.VariableDeclaration:
case nodes.NodeType.FunctionParameter:
return DocumentHighlightKind.Write;
}
}
return DocumentHighlightKind.Read;
}
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import {TextDocument, Range, Diagnostic, DiagnosticSeverity} from 'vscode-languageserver';
import {ILintConfigurationSettings, sanitize} from './lintRules';
import {LintVisitor} from './lint';
export interface LanguageSettings {
validate?: boolean;
lint?: ILintConfigurationSettings;
}
// The settings interface describe the server relevant settings part
export interface Settings {
css: LanguageSettings;
less: LanguageSettings;
scss: LanguageSettings;
}
export class CSSValidation {
private lintSettings: ILintConfigurationSettings;
private validationEnabled: boolean;
constructor() {
}
public configure(raw: LanguageSettings) {
if (raw) {
this.validationEnabled = raw.validate;
if (raw.lint) {
this.lintSettings = sanitize(raw.lint);
} else {
this.lintSettings = {};
}
}
}
public doValidation(document: TextDocument, stylesheet: nodes.Stylesheet): Thenable<Diagnostic[]> {
if (!this.validationEnabled) {
return Promise.resolve([]);
}
let entries: nodes.IMarker[] = [];
entries.push.apply(entries, nodes.ParseErrorCollector.entries(stylesheet));
entries.push.apply(entries, LintVisitor.entries(stylesheet, this.lintSettings));
function toDiagnostic(marker: nodes.IMarker): Diagnostic {
let range = Range.create(document.positionAt(marker.getOffset()), document.positionAt(marker.getOffset() + marker.getLength()));
return <Diagnostic>{
code: marker.getRule().id,
message: marker.getMessage(),
severity: marker.getLevel() === nodes.Level.Warning ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
range: range
};
}
return Promise.resolve(entries.filter(entry => entry.getLevel() !== nodes.Level.Ignore).map(toDiagnostic));
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import * as browsers from '../data/browsers';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export let colors : { [name:string]:string } = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgrey: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
grey: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370d8',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#d87093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
red: '#ff0000',
rebeccapurple: '#663399',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32'
};
export let colorKeywords : { [name:string]:string } = {
'currentColor': 'The value of the \'color\' property. The computed value of the \'currentColor\' keyword is the computed value of the \'color\' property. If the \'currentColor\' keyword is set on the \'color\' property itself, it is treated as \'color:inherit\' at parse time.',
'transparent': 'Fully transparent. This keyword can be considered a shorthand for rgba(0,0,0,0) which is its computed value.',
};
export let positionKeywords : { [name:string]:string } = {
'bottom': 'Computes to ‘100%’ for the vertical position if one or two values are given, otherwise specifies the bottom edge as the origin for the next offset.',
'center': 'Computes to ‘50%’ (‘left 50%’) for the horizontal position if the horizontal position is not otherwise specified, or ‘50%’ (‘top 50%’) for the vertical position if it is.',
'left': 'Computes to ‘0%’ for the horizontal position if one or two values are given, otherwise specifies the left edge as the origin for the next offset.',
'right': 'Computes to ‘100%’ for the horizontal position if one or two values are given, otherwise specifies the right edge as the origin for the next offset.',
'top': 'Computes to ‘0%’ for the vertical position if one or two values are given, otherwise specifies the top edge as the origin for the next offset.'
};
export let repeatStyleKeywords : { [name:string]:string } = {
'no-repeat': 'Placed once and not repeated in this direction.',
'repeat': 'Repeated in this direction as often as needed to cover the background painting area.',
'repeat-x': 'Computes to ‘repeat no-repeat’.',
'repeat-y': 'Computes to ‘no-repeat repeat’.',
'round': 'Repeated as often as will fit within the background positioning area. If it doesn’t fit a whole number of times, it is rescaled so that it does.',
'space': 'Repeated as often as will fit within the background positioning area without being clipped and then the images are spaced out to fill the area.'
};
export let lineStyleKeywords : { [name:string]:string } = {
'dashed': 'A series of square-ended dashes.',
'dotted': 'A series of round dots.',
'double': 'Two parallel solid lines with some space between them.',
'groove': 'Looks as if it were carved in the canvas.',
'hidden': 'Same as ‘none’, but has different behavior in the border conflict resolution rules for border-collapsed tables.',
'inset': 'Looks as if the content on the inside of the border is sunken into the canvas.',
'none': 'No border. Color and width are ignored.',
'outset': 'Looks as if the content on the inside of the border is coming out of the canvas.',
'ridge': 'Looks as if it were coming out of the canvas.',
'solid': 'A single line segment.'
};
export let lineWidthKeywords = ['medium', 'thick', 'thin'];
export let boxKeywords : { [name:string]:string } = {
'border-box': 'The background is painted within (clipped to) the border box.',
'content-box': 'The background is painted within (clipped to) the content box.',
'padding-box': 'The background is painted within (clipped to) the padding box.'
};
export let geometryBoxKeywords : { [name:string]:string } = {
'margin-box': 'Uses the margin box as reference box.',
'fill-box': 'Uses the object bounding box as reference box.',
'stroke-box': 'Uses the stroke bounding box as reference box.',
'view-box': 'Uses the nearest SVG viewport as reference box.'
};
export let cssWideKeywords : { [name:string]:string } = {
'initial': 'Represents the value specified as the property’s initial value.',
'inherit': 'Represents the computed value of the property on the element’s parent.',
'unset': 'Acts as either `inherit` or `initial`, depending on whether the property is inherited or not.'
};
export let colorFunctions = [
{ func: 'rgb($red, $green, $blue)', desc: localize('css.builtin.rgb', 'Creates a Color from red, green, and blue values.') },
{ func: 'rgba($red, $green, $blue, $alpha)', desc: localize('css.builtin.rgba', 'Creates a Color from red, green, blue, and alpha values.') },
{ func: 'hsl($hue, $saturation, $lightness)', desc: localize('css.builtin.hsl', 'Creates a Color from hue, saturation, and lightness values.') },
{ func: 'hsla($hue, $saturation, $lightness, $alpha)', desc: localize('css.builtin.hsla', 'Creates a Color from hue, saturation, lightness, and alpha values.') }
];
export let imageFunctions : { [name:string]:string } = {
'url()': 'Reference an image file by URL',
'image()': 'Provide image fallbacks and annotations.',
'-webkit-image-set()': 'Provide multiple resolutions. Remember to use unprefixed image-set() in addition.',
'image-set()': 'Provide multiple resolutions of an image and let the UA decide which is most appropriate in a given situation.',
'-moz-element()': 'Use an element in the document as an image. Remember to use unprefixed element() in addition.',
'element()': 'Use an element in the document as an image.',
'cross-fade()': 'Indicates the two images to be combined and how far along in the transition the combination is.',
'-webkit-gradient()': 'Deprecated. Use modern linear-gradient() or radial-gradient() instead.',
'-webkit-linear-gradient()': 'Linear gradient. Remember to use unprefixed version in addition.',
'-moz-linear-gradient()': 'Linear gradient. Remember to use unprefixed version in addition.',
'-o-linear-gradient()': 'Linear gradient. Remember to use unprefixed version in addition.',
'linear-gradient()': 'A linear gradient is created by specifying a straight gradient line, and then several colors placed along that line.',
'-webkit-repeating-linear-gradient()': 'Repeating Linear gradient. Remember to use unprefixed version in addition.',
'-moz-repeating-linear-gradient()': 'Repeating Linear gradient. Remember to use unprefixed version in addition.',
'-o-repeating-linear-gradient()': 'RepeatingLinear gradient. Remember to use unprefixed version in addition.',
'repeating-linear-gradient()': 'Same as linear-gradient, except the color-stops are repeated infinitely in both directions, with their positions shifted by multiples of the difference between the last specified color-stop’s position and the first specified color-stop’s position.',
'-webkit-radial-gradient()': 'Radial gradient. Remember to use unprefixed version in addition.',
'-moz-radial-gradient()': 'Radial gradient. Remember to use unprefixed version in addition.',
'radial-gradient()': 'Colors emerge from a single point and smoothly spread outward in a circular or elliptical shape.',
'-webkit-repeating-radial-gradient()': 'Repeating radial gradient. Remember to use unprefixed version in addition.',
'-moz-repeating-radial-gradient()': 'Repeating radial gradient. Remember to use unprefixed version in addition.',
'repeating-radial-gradient()': 'Same as radial-gradient, except the color-stops are repeated infinitely in both directions, with their positions shifted by multiples of the difference between the last specified color-stop’s position and the first specified color-stop’s position.'
};
export let transitionTimingFunctions : { [name:string]:string } = {
'ease': 'Equivalent to cubic-bezier(0.25, 0.1, 0.25, 1.0).',
'ease-in': 'Equivalent to cubic-bezier(0.42, 0, 1.0, 1.0).',
'ease-in-out': 'Equivalent to cubic-bezier(0.42, 0, 0.58, 1.0).',
'ease-out': 'Equivalent to cubic-bezier(0, 0, 0.58, 1.0).',
'linear': 'Equivalent to cubic-bezier(0.0, 0.0, 1.0, 1.0).',
'step-end': 'Equivalent to steps(1, end).',
'step-start': 'Equivalent to steps(1, start).',
'steps()': 'The first parameter specifies the number of intervals in the function. The second parameter, which is optional, is either the value “start” or “end”.',
'cubic-bezier()': 'Specifies a cubic-bezier curve. The four values specify points P1 and P2 of the curve as (x1, y1, x2, y2).',
'cubic-bezier(0.6, -0.28, 0.735, 0.045)': 'Ease-in Back. Overshoots.',
'cubic-bezier(0.68, -0.55, 0.265, 1.55)': 'Ease-in-out Back. Overshoots.',
'cubic-bezier(0.175, 0.885, 0.32, 1.275)': 'Ease-out Back. Overshoots.',
'cubic-bezier(0.6, 0.04, 0.98, 0.335)': 'Ease-in Circular. Based on half circle.',
'cubic-bezier(0.785, 0.135, 0.15, 0.86)': 'Ease-in-out Circular. Based on half circle.',
'cubic-bezier(0.075, 0.82, 0.165, 1)': 'Ease-out Circular. Based on half circle.',
'cubic-bezier(0.55, 0.055, 0.675, 0.19)': 'Ease-in Cubic. Based on power of three.',
'cubic-bezier(0.645, 0.045, 0.355, 1)': 'Ease-in-out Cubic. Based on power of three.',
'cubic-bezier(0.215, 0.610, 0.355, 1)': 'Ease-out Cubic. Based on power of three.',
'cubic-bezier(0.95, 0.05, 0.795, 0.035)': 'Ease-in Exponential. Based on two to the power ten.',
'cubic-bezier(1, 0, 0, 1)': 'Ease-in-out Exponential. Based on two to the power ten.',
'cubic-bezier(0.19, 1, 0.22, 1)': 'Ease-out Exponential. Based on two to the power ten.',
'cubic-bezier(0.47, 0, 0.745, 0.715)': 'Ease-in Sine.',
'cubic-bezier(0.445, 0.05, 0.55, 0.95)': 'Ease-in-out Sine.',
'cubic-bezier(0.39, 0.575, 0.565, 1)': 'Ease-out Sine.',
'cubic-bezier(0.55, 0.085, 0.68, 0.53)': 'Ease-in Quadratic. Based on power of two.',
'cubic-bezier(0.455, 0.03, 0.515, 0.955)': 'Ease-in-out Quadratic. Based on power of two.',
'cubic-bezier(0.25, 0.46, 0.45, 0.94)': 'Ease-out Quadratic. Based on power of two.',
'cubic-bezier(0.895, 0.03, 0.685, 0.22)': 'Ease-in Quartic. Based on power of four.',
'cubic-bezier(0.77, 0, 0.175, 1)': 'Ease-in-out Quartic. Based on power of four.',
'cubic-bezier(0.165, 0.84, 0.44, 1)': 'Ease-out Quartic. Based on power of four.',
'cubic-bezier(0.755, 0.05, 0.855, 0.06)': 'Ease-in Quintic. Based on power of five.',
'cubic-bezier(0.86, 0, 0.07, 1)': 'Ease-in-out Quintic. Based on power of five.',
'cubic-bezier(0.23, 1, 0.320, 1)': 'Ease-out Quintic. Based on power of five.'
};
export let basicShapeFunctions : { [name:string]:string } = {
'circle()': 'Defines a circle.',
'ellipse()': 'Defines an ellipse.',
'inset()': 'Defines an inset rectangle.',
'polygon()': 'Defines a polygon.'
};
export let units : { [unitName:string]:string[] } = {
'length': ['em', 'rem', 'ex', 'px', 'cm', 'mm', 'in', 'pt', 'pc', 'ch', 'vw', 'vh', 'vmin', 'vmax'],
'angle': ['deg', 'rad', 'grad', 'turn'],
'time': ['ms', 's'],
'frequency': ['Hz', 'kHz'],
'resolution': ['dpi', 'dpcm', 'dppx'],
'percentage': ['%']
};
export let html5Tags = ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption',
'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer',
'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
'main', 'map', 'mark', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q',
'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td',
'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'let', 'video', 'wbr' ];
export let svgElements = ['circle', 'clipPath', 'cursor', 'defs', 'desc', 'ellipse', 'feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting',
'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology',
'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence', 'filter', 'foreignObject', 'g', 'hatch', 'hatchpath', 'image', 'line', 'linearGradient',
'marker', 'mask', 'mesh', 'meshpatch', 'meshrow', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'solidcolor', 'stop', 'svg', 'switch',
'symbol', 'text', 'textPath', 'tspan', 'use', 'view'];
export function isColorConstructor(node:nodes.Function): boolean {
let name = node.getName();
if (!name) {
return false;
}
return /^(rgb|rgba|hsl|hsla)$/gi.test(name);
}
/**
* Returns true if the node is a color value - either
* defined a hex number, as rgb or rgba function, or
* as color name.
*/
export function isColorValue(node:nodes.Node):boolean {
if(node.type === nodes.NodeType.HexColorValue) {
return true;
} else if(node.type === nodes.NodeType.Function) {
return this.isColorConstructor(<nodes.Function> node);
} else if (node.type === nodes.NodeType.Identifier) {
if (node.parent && node.parent.type !== nodes.NodeType.Term) {
return false;
}
let candidateColor = node.getText().toLowerCase();
if(candidateColor === 'none') {
return false;
}
if (colors[candidateColor]) {
return true;
}
}
return false;
}
/**
* Returns true if the given name is a known property.
*/
export function isKnownProperty(name: string):boolean {
if(!name) {
return false;
} else {
name = name.toLowerCase();
return getProperties().hasOwnProperty(name);
}
}
export function isCommonValue(entry:Value):boolean {
return entry.browsers.count > 1;
}
export function getPageBoxDirectives():string[] {
return [
'@bottom-center', '@bottom-left', '@bottom-left-corner', '@bottom-right', '@bottom-right-corner',
'@left-bottom', '@left-middle', '@left-top', '@right-bottom', '@right-middle', '@right-top',
'@top-center', '@top-left', '@top-left-corner', '@top-right', '@top-right-corner'
];
}
export function getEntryDescription(entry:{description: string; browsers: Browsers}): string {
let desc = entry.description || '';
let browserLabel = this.getBrowserLabel(entry.browsers);
if (browserLabel) {
if (desc) {
desc = desc + '\n';
}
desc= desc + '(' + browserLabel + ')';
}
return desc;
}
export function getBrowserLabel(b: Browsers): string {
let result = '';
if (!b || b.all || b.count === 0) {
return null;
}
for (let curr in browserNames) {
if (typeof (<any> b)[curr] === 'string') {
if (result.length > 0) {
result = result + ', ';
}
result = result + (<any> browserNames)[curr];
let version = (<any> b)[curr];
if (version.length > 0) {
result = result + ' ' + version;
}
}
}
return result;
}
export interface Browsers {
E?:string;
FF?:string;
IE?:string;
O?:string;
C?:string;
S?:string;
count:number;
all:boolean;
onCodeComplete:boolean;
}
export interface Value {
name:string;
description:string;
browsers:Browsers;
}
export interface IEntry {
name:string;
restrictions:string[];
browsers:Browsers;
description:string;
values:Value[];
}
function evalBrowserEntry(browsers: string) {
let browserEntry : Browsers = { all: false, count: 0, onCodeComplete: false};
let count = 0;
if (browsers) {
browsers.split(',').forEach(
(s: string) => {
s = s.trim();
if (s === 'all') {
browserEntry.all= true;
count = Number.MAX_VALUE;
} else if (s !== 'none') {
for (let key in browserNames) {
if (s.indexOf(key) === 0) {
(<any> browserEntry)[key] = s.substring(key.length).trim();
count++;
}
}
}
}
);
} else {
browserEntry.all = true;
count = Number.MAX_VALUE;
}
browserEntry.count = count;
browserEntry.onCodeComplete = count > 0; // to be refined
return browserEntry;
}
class ValueImpl implements Value {
private browserEntry: Browsers;
constructor(public data: any) {
}
get name() : string {
return this.data.name;
}
get description() : string {
return this.data.desc || browsers.descriptions[this.data.name];
}
get browsers() : Browsers {
if (!this.browserEntry) {
this.browserEntry = evalBrowserEntry(this.data.browsers);
}
return this.browserEntry;
}
}
class EntryImpl implements IEntry {
private browserEntry: Browsers;
constructor(public data: any) {
}
get name(): string {
return this.data.name;
}
get description(): string {
return this.data.desc || browsers.descriptions[this.data.name];
}
get browsers(): Browsers {
if (!this.browserEntry) {
this.browserEntry = evalBrowserEntry(this.data.browsers);
}
return this.browserEntry;
}
get restrictions(): string[] {
if (this.data.restriction) {
return this.data.restriction.split(',').map(function(s: string) { return s.trim(); });
} else {
return [];
}
}
get values(): Value[] {
if(!this.data.values) {
return [];
}
if(!Array.isArray(this.data.values)) {
return [new ValueImpl(this.data.values.value)];
}
return this.data.values.map(function (v: string) {
return new ValueImpl(v);
});
}
}
let propertySet: { [key: string]: IEntry };
let properties = browsers.data.css.properties;
export function getProperties(): { [name: string]: IEntry; } {
if(!propertySet) {
propertySet = {
};
for(let i = 0, len = properties.length; i < len; i++) {
let rawEntry = properties[i];
propertySet[rawEntry.name] = new EntryImpl(rawEntry);
}
}
return propertySet;
}
let atDirectives = browsers.data.css.atdirectives;
let atDirectiveList: IEntry[];
export function getAtDirectives(): IEntry[] {
if (!atDirectiveList) {
atDirectiveList = [];
for (let i = 0, len = atDirectives.length; i < len; i++) {
let rawEntry = atDirectives[i];
atDirectiveList.push(new EntryImpl(rawEntry));
}
}
return atDirectiveList;
}
let pseudoElements = browsers.data.css.pseudoelements;
let pseudoElementList: IEntry[];
export function getPseudoElements(): IEntry[] {
if (!pseudoElementList) {
pseudoElementList = [];
for (let i = 0, len = pseudoElements.length; i < len; i++) {
let rawEntry = pseudoElements[i];
pseudoClassesList.push(new EntryImpl(rawEntry));
}
}
return pseudoElementList;
}
let pseudoClasses = browsers.data.css.pseudoclasses;
let pseudoClassesList: IEntry[];
export function getPseudoClasses(): IEntry[]{
if (!pseudoClassesList) {
pseudoClassesList = [];
for (let i = 0, len = pseudoClasses.length; i < len; i++) {
let rawEntry = pseudoClasses[i];
pseudoClassesList.push(new EntryImpl(rawEntry));
}
}
return pseudoClassesList;
}
export let browserNames = {
E : 'Edge',
FF : 'Firefox',
S : 'Safari',
C : 'Chrome',
IE : 'IE',
O : 'Opera'
};
/*---------------------------------------------------------------------------------------------
* 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 {Rules, ILintConfigurationSettings, toLevel, Rule} from './lintRules';
import * as nodes from '../parser/cssNodes';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
class Element {
public name: string;
public node: nodes.Declaration;
constructor(text: string, data: nodes.Declaration) {
this.name = text;
this.node = data;
}
}
class NodesByRootMap {
public data: { [name: string]: { nodes: nodes.Node[]; names: string[] } } = {};
public add(root: string, name: string, node: nodes.Node): void {
let entry = this.data[root];
if (!entry) {
entry = { nodes: [], names: [] };
this.data[root] = entry;
}
entry.names.push(name);
if (node) {
entry.nodes.push(node);
}
}
}
export class LintVisitor implements nodes.IVisitor {
static entries(node: nodes.Node, settings: ILintConfigurationSettings): nodes.IMarker[] {
let visitor = new LintVisitor(settings);
node.accept(visitor);
return visitor.getEntries();
}
static prefixes = [
'-ms-', '-moz-', '-o-', '-webkit-', // Quite common
// '-xv-', '-atsc-', '-wap-', '-khtml-', 'mso-', 'prince-', '-ah-', '-hp-', '-ro-', '-rim-', '-tc-' // Quite un-common
];
private warnings: nodes.IMarker[] = [];
private configuration: { [id: string]: nodes.Level };
constructor(settings: ILintConfigurationSettings = {}) {
this.configuration = {};
for (let ruleKey in Rules) {
let rule = Rules[ruleKey];
let level = settings[rule.id] || toLevel(rule.defaultValue);
this.configuration[rule.id] = level;
}
}
private fetch(input: Element[], s: string): Element[] {
let elements: Element[] = [];
for (let i = 0; i < input.length; i++) {
if (input[i].name.toLowerCase() === s) {
elements.push(input[i]);
}
}
return elements;
}
private fetchWithValue(input: Element[], s: string, v: string): Element[] {
let elements: Element[] = [];
for (let inputElement of input) {
if (inputElement.name.toLowerCase() === s) {
let expression = inputElement.node.getValue();
if (expression && this.findValueInExpression(expression, v)) {
elements.push(inputElement);
}
}
}
return elements;
}
private findValueInExpression(expression: nodes.Expression, v: string): boolean {
let found = false;
expression.accept(node => {
if (node.type === nodes.NodeType.Identifier && node.getText() === v) {
found = true;
}
return !found;
});
return found;
}
private fetchWithin(input: Element[], s: string): Element[] {
let elements: Element[] = [];
for (let inputElement of input) {
if (inputElement.name.toLowerCase().indexOf(s) >= 0) {
elements.push(inputElement);
}
}
return elements;
}
public getEntries(filter: number = (nodes.Level.Warning | nodes.Level.Error)): nodes.IMarker[] {
return this.warnings.filter(entry => {
return (entry.getLevel() & filter) !== 0;
});
}
private addEntry(node: nodes.Node, rule: Rule, details?: string): void {
let entry = new nodes.Marker(node, rule, this.configuration[rule.id], details);
this.warnings.push(entry);
}
private getMissingNames(expected: string[], actual: string[]): string {
expected = expected.slice(0); // clone
for (let i = 0; i < actual.length; i++) {
let k = expected.indexOf(actual[i]);
if (k !== -1) {
expected[k] = null;
}
}
let result: string = null;
for (let i = 0; i < expected.length; i++) {
let curr = expected[i];
if (curr) {
if (result === null) {
result = localize('namelist.single', "'{0}'", curr);
} else {
result = localize('namelist.concatenated', "{0}, '{1}'", result, curr);
}
}
}
return result;
}
public visitNode(node: nodes.Node): boolean {
switch (node.type) {
case nodes.NodeType.Stylesheet:
return this.visitStylesheet(<nodes.Stylesheet>node);
case nodes.NodeType.FontFace:
return this.visitFontFace(<nodes.FontFace>node);
case nodes.NodeType.Ruleset:
return this.visitRuleSet(<nodes.RuleSet>node);
case nodes.NodeType.SimpleSelector:
return this.visitSimpleSelector(<nodes.SimpleSelector>node);
case nodes.NodeType.Function:
return this.visitFunction(<nodes.Function>node);
case nodes.NodeType.NumericValue:
return this.visitNumericValue(<nodes.NumericValue>node);
case nodes.NodeType.Import:
return this.visitImport(<nodes.Import>node);
}
return this.visitUnknownNode(node);
}
private visitStylesheet(node: nodes.Stylesheet): boolean {
// @keyframe and it's vendor specific alternatives
// @keyframe should be included
let keyframes = new NodesByRootMap();
node.accept(node => {
if (node instanceof nodes.Keyframe) {
let keyword = (<nodes.Keyframe>node).getKeyword();
let text = keyword.getText();
keyframes.add((<nodes.Keyframe>node).getName(), text, (text !== '@keyframes') ? keyword : null);
}
return true;
});
let expected = ['@-webkit-keyframes', '@-moz-keyframes', '@-o-keyframes'];
for (let name in keyframes.data) {
let actual = keyframes.data[name].names;
let needsStandard = (actual.indexOf('@keyframes') === -1);
if (!needsStandard && actual.length === 1) {
continue; // only the non-vendor specific keyword is used, that's fine, no warning
}
let addVendorSpecificWarnings = (node: nodes.Node) => {
if (needsStandard) {
let message = localize('keyframes.standardrule.missing', "Always define standard rule '@keyframes' when defining keyframes.");
this.addEntry(node, Rules.IncludeStandardPropertyWhenUsingVendorPrefix, message);
}
if (missingVendorSpecific) {
let message = localize('keyframes.vendorspecific.missing', "Always include all vendor specific rules: Missing: {0}", missingVendorSpecific);
this.addEntry(node, Rules.AllVendorPrefixes, message);
}
};
let missingVendorSpecific = this.getMissingNames(expected, actual);
if (missingVendorSpecific || needsStandard) {
keyframes.data[name].nodes.forEach(addVendorSpecificWarnings);
}
}
return true;
}
private visitSimpleSelector(node: nodes.SimpleSelector): boolean {
let text = node.getText();
/////////////////////////////////////////////////////////////
// Lint - The universal selector (*) is known to be slow.
/////////////////////////////////////////////////////////////
if (text === '*') {
this.addEntry(node, Rules.UniversalSelector);
}
/////////////////////////////////////////////////////////////
// Lint - Avoid id selectors
/////////////////////////////////////////////////////////////
if (text.indexOf('#') === 0) {
this.addEntry(node, Rules.AvoidIdSelector);
}
return true;
}
private visitImport(node: nodes.Import): boolean {
/////////////////////////////////////////////////////////////
// Lint - Import statements shouldn't be used, because they aren't offering parallel downloads.
/////////////////////////////////////////////////////////////
this.addEntry(node, Rules.ImportStatemement);
return true;
}
private visitRuleSet(node: nodes.RuleSet): boolean {
/////////////////////////////////////////////////////////////
// Lint - Don't use empty rulesets.
/////////////////////////////////////////////////////////////
let declarations = node.getDeclarations();
if (!declarations) {
// syntax error
return false;
}
if (!declarations.hasChildren()) {
this.addEntry(node.getSelectors(), Rules.EmptyRuleSet);
}
let self = this;
let propertyTable: Element[] = [];
declarations.getChildren().forEach(function (element) {
if (element instanceof nodes.Declaration) {
let decl = <nodes.Declaration>element;
propertyTable.push(new Element(decl.getFullPropertyName(), decl));
}
}, this);
/////////////////////////////////////////////////////////////
// Don't use width or height when using padding or border.
/////////////////////////////////////////////////////////////
if ((this.fetch(propertyTable, 'width').length > 0 || this.fetch(propertyTable, 'height').length > 0) && (this.fetchWithin(propertyTable, 'padding').length > 0 || this.fetchWithin(propertyTable, 'border').length > 0)) {
let elements: Element[] = this.fetch(propertyTable, 'width');
for (let index = 0; index < elements.length; index++) {
this.addEntry(elements[index].node, Rules.NoWidthOrHeightWhenPaddingOrBorder);
}
elements = this.fetch(propertyTable, 'height');
for (let index = 0; index < elements.length; index++) {
this.addEntry(elements[index].node, Rules.NoWidthOrHeightWhenPaddingOrBorder);
}
elements = this.fetchWithin(propertyTable, 'padding');
for (let index = 0; index < elements.length; index++) {
this.addEntry(elements[index].node, Rules.NoWidthOrHeightWhenPaddingOrBorder);
}
elements = this.fetchWithin(propertyTable, 'border');
for (let index = 0; index < elements.length; index++) {
this.addEntry(elements[index].node, Rules.NoWidthOrHeightWhenPaddingOrBorder);
}
}
/////////////////////////////////////////////////////////////
// Properties ignored due to display
/////////////////////////////////////////////////////////////
// With 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect
let displayElems = this.fetchWithValue(propertyTable, 'display', 'inline');
if (displayElems.length > 0) {
['width', 'height', 'margin-top', 'margin-bottom', 'float'].forEach(function (prop) {
let elem = self.fetch(propertyTable, prop);
for (let index = 0; index < elem.length; index++) {
self.addEntry(elem[index].node, Rules.PropertyIgnoredDueToDisplay);
}
});
}
// With 'display: inline-block', 'float' has no effect
displayElems = this.fetchWithValue(propertyTable, 'display', 'inline-block');
if (displayElems.length > 0) {
let elem = this.fetch(propertyTable, 'float');
for (let index = 0; index < elem.length; index++) {
this.addEntry(elem[index].node, Rules.PropertyIgnoredDueToDisplay);
}
}
// With 'display: block', 'vertical-align' has no effect
displayElems = this.fetchWithValue(propertyTable, 'display', 'block');
if (displayElems.length > 0) {
let elem = this.fetch(propertyTable, 'vertical-align');
for (let index = 0; index < elem.length; index++) {
this.addEntry(elem[index].node, Rules.PropertyIgnoredDueToDisplay);
}
}
/////////////////////////////////////////////////////////////
// Don't use !important
/////////////////////////////////////////////////////////////
node.accept(n => {
if (n.type === nodes.NodeType.Prio) {
self.addEntry(n, Rules.AvoidImportant);
}
return true;
});
/////////////////////////////////////////////////////////////
// Avoid 'float'
/////////////////////////////////////////////////////////////
let elements: Element[] = this.fetch(propertyTable, 'float');
for (let index = 0; index < elements.length; index++) {
this.addEntry(elements[index].node, Rules.AvoidFloat);
}
/////////////////////////////////////////////////////////////
// Don't use duplicate declarations.
/////////////////////////////////////////////////////////////
for (let i = 0; i < propertyTable.length; i++) {
let element = propertyTable[i];
if (element.name.toLowerCase() !== 'background') {
let value = element.node.getValue();
if (value && value.getText()[0] !== '-') {
let elements = this.fetch(propertyTable, element.name);
if (elements.length > 1) {
for (let k = 0; k < elements.length; k++) {
let value = elements[k].node.getValue();
if (value && value.getText()[0] !== '-' && elements[k] !== element) {
this.addEntry(element.node, Rules.DuplicateDeclarations);
}
}
}
}
}
}
/////////////////////////////////////////////////////////////
// Unknown propery & When using a vendor-prefixed gradient, make sure to use them all.
/////////////////////////////////////////////////////////////
let propertiesBySuffix = new NodesByRootMap();
let containsUnknowns = false;
declarations.getChildren().forEach(node => {
if (this.isCSSDeclaration(node)) {
let decl = <nodes.Declaration>node;
let name = decl.getFullPropertyName();
let firstChar = name.charAt(0);
if (firstChar === '-') {
if (name.charAt(1) !== '-') { // avoid css variables
if (!languageFacts.isKnownProperty(name)) {
this.addEntry(decl.getProperty(), Rules.UnknownVendorSpecificProperty);
}
let nonPrefixedName = decl.getNonPrefixedPropertyName();
propertiesBySuffix.add(nonPrefixedName, name, decl.getProperty());
}
} else {
if (firstChar === '*' || firstChar === '_') {
this.addEntry(decl.getProperty(), Rules.IEStarHack);
name = name.substr(1);
}
if (!languageFacts.isKnownProperty(name)) {
this.addEntry(decl.getProperty(), Rules.UnknownProperty);
}
propertiesBySuffix.add(name, name, null); // don't pass the node as we don't show errors on the standard
}
} else {
containsUnknowns = true;
}
});
if (!containsUnknowns) { // don't perform this test if there are
for (let suffix in propertiesBySuffix.data) {
let entry = propertiesBySuffix.data[suffix];
let actual = entry.names;
let needsStandard = languageFacts.isKnownProperty(suffix) && (actual.indexOf(suffix) === -1);
if (!needsStandard && actual.length === 1) {
continue; // only the non-vendor specific rule is used, that's fine, no warning
}
let expected: string[] = [];
for (let i = 0, len = LintVisitor.prefixes.length; i < len; i++) {
let prefix = LintVisitor.prefixes[i];
if (languageFacts.isKnownProperty(prefix + suffix)) {
expected.push(prefix + suffix);
}
}
let addVendorSpecificWarnings = (node: nodes.Node) => {
if (needsStandard) {
let message = localize('property.standard.missing', "Also define the standard property '{0}' for compatibility", suffix);
this.addEntry(node, Rules.IncludeStandardPropertyWhenUsingVendorPrefix, message);
}
if (missingVendorSpecific) {
let message = localize('property.vendorspecific.missing', "Always include all vendor specific properties: Missing: {0}", missingVendorSpecific);
this.addEntry(node, Rules.AllVendorPrefixes, message);
}
};
let missingVendorSpecific = this.getMissingNames(expected, actual);
if (missingVendorSpecific || needsStandard) {
entry.nodes.forEach(addVendorSpecificWarnings);
}
}
}
return true;
}
private visitNumericValue(node: nodes.NumericValue): boolean {
/////////////////////////////////////////////////////////////
// 0 has no following unit
/////////////////////////////////////////////////////////////
let value = node.getValue();
if (value.unit === '%') {
return true;
}
if (parseFloat(value.value) === 0.0 && !!value.unit) {
this.addEntry(node, Rules.ZeroWithUnit);
}
return true;
}
private visitFontFace(node: nodes.FontFace): boolean {
let declarations = node.getDeclarations();
if (!declarations) {
// syntax error
return;
}
let definesSrc = false, definesFontFamily = false;
let containsUnknowns = false;
declarations.getChildren().forEach(node => {
if (this.isCSSDeclaration(node)) {
let name = ((<nodes.Declaration>node).getProperty().getName().toLocaleLowerCase());
if (name === 'src') { definesSrc = true; }
if (name === 'font-family') { definesFontFamily = true; }
} else {
containsUnknowns = true;
}
});
if (!containsUnknowns && (!definesSrc || !definesFontFamily)) {
this.addEntry(node, Rules.RequiredPropertiesForFontFace);
}
return true;
}
private isCSSDeclaration(node: nodes.Node): boolean {
if (node instanceof nodes.Declaration) {
if (!(<nodes.Declaration>node).getValue()) {
return false;
}
let property = (<nodes.Declaration>node).getProperty();
if (!property || property.getIdentifier().containsInterpolation()) {
return false;
}
return true;
}
return false;
}
private visitUnknownNode(node: nodes.Node): boolean {
// Rule: #eeff00 or #ef0
if (node.type === nodes.NodeType.HexColorValue) {
let text = node.getText();
if (text.length !== 7 && text.length !== 4) {
this.addEntry(node, Rules.HexColorLength);
}
}
return true;
}
private visitFunction(node: nodes.Function): boolean {
let fnName = node.getName().toLowerCase();
let expectedAttrCount = -1;
let actualAttrCount = 0;
switch (fnName) {
case 'rgb(':
case 'hsl(':
expectedAttrCount = 3;
break;
case 'rgba(':
case 'hsla(':
expectedAttrCount = 4;
break;
}
if (expectedAttrCount !== -1) {
node.getArguments().accept(n => {
if (n instanceof nodes.BinaryExpression) {
actualAttrCount += 1;
return false;
}
return true;
});
if (actualAttrCount !== expectedAttrCount) {
this.addEntry(node, Rules.ArgsInColorFunction);
}
}
return true;
}
}
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
let Warning = 'warning';
let Error = 'error';
let Ignore = 'ignore';
export class Rule implements nodes.IRule {
public constructor(public id: string, public message: string, public defaultValue: string) {
// nothing to do
}
}
export let Rules = {
AllVendorPrefixes: new Rule('compatibleVendorPrefixes', localize('rule.vendorprefixes.all', "When using a vendor-specific prefix make sure to also include all other vendor-specific properties"), Ignore),
IncludeStandardPropertyWhenUsingVendorPrefix: new Rule('vendorPrefix', localize('rule.standardvendorprefix.all', "When using a vendor-specific prefix also include the standard property"), Warning),
DuplicateDeclarations: new Rule('duplicateProperties', localize('rule.duplicateDeclarations', "Do not use duplicate style definitions"), Ignore),
EmptyRuleSet: new Rule('emptyRules', localize('rule.emptyRuleSets', "Do not use empty rulesets"), Warning),
ImportStatemement: new Rule('importStatement', localize('rule.importDirective', "Import statements do not load in parallel"), Ignore),
NoWidthOrHeightWhenPaddingOrBorder: new Rule('boxModel', localize('rule.withHeightAndBorderPadding', "Do not use width or height when using padding or border"), Ignore),
UniversalSelector: new Rule('universalSelector', localize('rule.universalSelector', "The universal selector (*) is known to be slow"), Ignore),
ZeroWithUnit: new Rule('zeroUnits', localize('rule.zeroWidthUnit', "No unit for zero needed"), Ignore),
RequiredPropertiesForFontFace: new Rule('fontFaceProperties', localize('rule.fontFaceProperties', "@font-face rule must define 'src' and 'font-family' properties"), Warning),
HexColorLength: new Rule('hexColorLength', localize('rule.hexColor', "Hex colors must consist of three or six hex numbers"), Error),
ArgsInColorFunction: new Rule('argumentsInColorFunction', localize('rule.colorFunction', "Invalid number of parameters"), Error),
UnknownProperty: new Rule('unknownProperties', localize('rule.unknownProperty', "Unknown property."), Warning),
IEStarHack: new Rule('ieHack', localize('rule.ieHack', "IE hacks are only necessary when supporting IE7 and older"), Ignore),
UnknownVendorSpecificProperty: new Rule('unknownVendorSpecificProperties', localize('rule.unknownVendorSpecificProperty', "Unknown vendor specific property."), Ignore),
PropertyIgnoredDueToDisplay: new Rule('propertyIgnoredDueToDisplay', localize('rule.propertyIgnoredDueToDisplay', "Property is ignored due to the display. E.g. with 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect"), Warning),
AvoidImportant: new Rule('important', localize('rule.avoidImportant', "Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored."), Ignore),
AvoidFloat: new Rule('float', localize('rule.avoidFloat', "Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes."), Ignore),
AvoidIdSelector: new Rule('idSelector', localize('rule.avoidIdSelector', "Selectors should not contain IDs because these rules are too tightly coupled with the HTML."), Ignore),
};
export interface ILintConfigurationSettings {
[ruleId:string] : nodes.Level;
}
export function sanitize(conf:any): ILintConfigurationSettings {
let settings: ILintConfigurationSettings = {};
for (let ruleName in Rules) {
let rule = Rules[ruleName];
let level = toLevel(conf[rule.id]);
if (level) {
settings[rule.id] = level;
}
}
return settings;
}
export function toLevel(level: string):nodes.Level {
switch (level) {
case 'ignore': return nodes.Level.Ignore;
case 'warning': return nodes.Level.Warning;
case 'error': return nodes.Level.Error;
}
return null;
}
/*---------------------------------------------------------------------------------------------
* 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 '../parser/cssNodes';
import {MarkedString} from 'vscode-languageserver';
export class Element {
public name: string;
public parent: Element;
public children: Element[];
public attributes: { [name: string]: string; };
public addChild(child: Element): void {
if (child instanceof Element) {
(<Element>child).parent = this;
}
if (!this.children) {
this.children = [];
}
this.children.push(child);
}
public findRoot(): Element {
let curr: Element = this;
while (curr.parent && !(curr.parent instanceof RootElement)) {
curr = curr.parent;
}
return curr;
}
public removeChild(child: Element): boolean {
if (this.children) {
let index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
return true;
}
}
return false;
}
public addAttr(name: string, value: string): void {
if (!this.attributes) {
this.attributes = {};
}
if (this.attributes.hasOwnProperty(name)) {
this.attributes[name] += ' ' + value;
} else {
this.attributes[name] = value;
}
}
public clone(cloneChildren: boolean = true): Element {
let elem = new Element();
elem.name = this.name;
if (this.attributes) {
elem.attributes = {};
for (let key in this.attributes) {
elem.addAttr(key, this.attributes[key]);
}
}
if (cloneChildren && this.children) {
elem.children = [];
for (let index = 0; index < this.children.length; index++) {
elem.addChild(this.children[index].clone());
}
}
return elem;
}
public cloneWithParent(): Element {
let clone = this.clone(false);
if (this.parent && !(this.parent instanceof RootElement)) {
let parentClone = this.parent.cloneWithParent();
parentClone.addChild(clone);
}
return clone;
}
}
export class RootElement extends Element {
}
export class LabelElement extends Element {
constructor(label: string) {
super();
this.name = label;
}
}
class MarkedStringPrinter {
private result: MarkedString[];
constructor(public quote: string) {
// empty
}
public print(element: Element): MarkedString[] {
this.result = [];
if (element instanceof RootElement) {
this.doPrint(element.children, 0);
} else {
this.doPrint([element], 0);
}
return this.result;
}
private doPrint(elements: Element[], indent: number) {
for (let element of elements) {
this.doPrintElement(element, indent);
if (element.children) {
this.doPrint(element.children, indent + 1);
}
}
}
private writeLine(level: number, content: string) {
let indent = new Array(level).join(' ');
this.result.push({ language: 'html', value: indent + content });
}
private doPrintElement(element: Element, indent: number) {
// special case: a simple label
if (element instanceof LabelElement) {
this.writeLine(indent, element.name);
return;
}
// the real deal
let content = ['<'];
// element name
if (element.name) {
content.push(element.name);
} else {
content.push('element');
}
// attributes
if (element.attributes) {
Object.keys(element.attributes).forEach((attr) => {
content.push(' ');
content.push(attr);
let value = element.attributes[attr];
if (value) {
content.push('=');
content.push(quotes.ensure(value, this.quote));
}
});
}
content.push('>');
this.writeLine(indent, content.join(''));
}
}
namespace quotes {
export function ensure(value: string, which: string): string {
return which + remove(value) + which;
}
export function remove(value: string): string {
let match = value.match(/^['"](.*)["']$/);
if (match) {
return match[1];
}
return value;
}
}
export function toElement(node: nodes.SimpleSelector, parentElement?: Element): Element {
let result = new Element();
node.getChildren().forEach((child) => {
switch (child.type) {
case nodes.NodeType.SelectorCombinator:
if (parentElement) {
let segments = child.getText().split('&');
if (segments.length === 1) {
// should not happen
result.name = segments[0];
break;
}
result = parentElement.cloneWithParent();
if (segments[0]) {
let root = result.findRoot();
root.name = segments[0] + root.name;
}
for (let i = 1; i < segments.length; i++) {
if (i > 1) {
let clone = parentElement.cloneWithParent();
result.addChild(clone.findRoot());
result = clone;
}
result.name += segments[i];
}
}
break;
case nodes.NodeType.SelectorPlaceholder:
case nodes.NodeType.ElementNameSelector:
let text = child.getText();
result.name = text === '*' ? 'element' : text;
break;
case nodes.NodeType.ClassSelector:
result.addAttr('class', child.getText().substring(1));
break;
case nodes.NodeType.IdentifierSelector:
result.addAttr('id', child.getText().substring(1));
break;
case nodes.NodeType.MixinDeclaration:
result.addAttr('class', (<nodes.MixinDeclaration>child).getName());
break;
case nodes.NodeType.PseudoSelector:
result.addAttr(child.getText(), '');
break;
case nodes.NodeType.AttributeSelector:
let expr = <nodes.BinaryExpression>child.getChildren()[0];
if (expr) {
let value: string;
if (expr.getRight()) {
switch (expr.getOperator().getText()) {
case '|=':
// excatly or followed by -words
value = `${quotes.remove(expr.getRight().getText())}-\u2026`;
break;
case '^=':
// prefix
value = `${quotes.remove(expr.getRight().getText())}\u2026`;
break;
case '$=':
// suffix
value = `\u2026${quotes.remove(expr.getRight().getText())}`;
break;
case '~=':
// one of a list of words
value = ` \u2026 ${quotes.remove(expr.getRight().getText())} \u2026 `;
break;
case '*=':
// substring
value = `\u2026${quotes.remove(expr.getRight().getText())}\u2026`;
break;
default:
value = quotes.remove(expr.getRight().getText());
break;
}
}
result.addAttr(expr.getLeft().getText(), value);
}
break;
}
});
return result;
}
export function selectorToMarkedString(node: nodes.Selector): MarkedString[] {
let root = selectorToElement(node);
return new MarkedStringPrinter('"').print(root);
}
export function simpleSelectorToMarkedString(node: nodes.SimpleSelector): MarkedString[] {
let element = toElement(node);
return new MarkedStringPrinter('"').print(element);
}
class SelectorElementBuilder {
private prev: nodes.Node;
private element: Element;
public constructor(element: Element) {
this.prev = null;
this.element = element;
}
public processSelector(selector: nodes.Selector): void {
let parentElement: Element = null;
if (!(this.element instanceof RootElement)) {
if (selector.getChildren().some((c) => c.hasChildren() && c.getChild(0).type === nodes.NodeType.SelectorCombinator)) {
let curr = this.element.findRoot();
if (curr.parent instanceof RootElement) {
parentElement = this.element;
this.element = curr.parent;
this.element.removeChild(curr);
this.prev = null;
}
}
}
selector.getChildren().forEach((selectorChild) => {
if (selectorChild instanceof nodes.SimpleSelector) {
if (this.prev instanceof nodes.SimpleSelector) {
let labelElement = new LabelElement('\u2026');
this.element.addChild(labelElement);
this.element = labelElement;
} else if (this.prev && (this.prev.matches('+') || this.prev.matches('~'))) {
this.element = <Element>this.element.parent;
}
if (this.prev && this.prev.matches('~')) {
this.element.addChild(toElement(<nodes.SimpleSelector>selectorChild));
this.element.addChild(new LabelElement('\u22EE'));
}
let thisElement = toElement(<nodes.SimpleSelector>selectorChild, parentElement);
let root = thisElement.findRoot();
this.element.addChild(root);
this.element = thisElement;
}
if (selectorChild instanceof nodes.SimpleSelector ||
selectorChild.type === nodes.NodeType.SelectorCombinatorParent ||
selectorChild.type === nodes.NodeType.SelectorCombinatorSibling ||
selectorChild.type === nodes.NodeType.SelectorCombinatorAllSiblings) {
this.prev = selectorChild;
}
});
}
}
function isNewSelectorContext(node: nodes.Node): boolean {
switch (node.type) {
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.Stylesheet:
return true;
}
return false;
}
export function selectorToElement(node: nodes.Selector): Element {
let root: Element = new RootElement();
let parentRuleSets: nodes.RuleSet[] = [];
if (node.getParent() instanceof nodes.RuleSet) {
let parent = node.getParent().getParent(); // parent of the selector's ruleset
while (parent && !isNewSelectorContext(parent)) {
if (parent instanceof nodes.RuleSet) {
parentRuleSets.push(<nodes.RuleSet>parent);
}
parent = parent.getParent();
}
}
let builder = new SelectorElementBuilder(root);
for (let i = parentRuleSets.length - 1; i >= 0; i--) {
let selector = parentRuleSets[i].getSelectors().getChild(0);
if (selector) {
builder.processSelector(selector);
}
}
builder.processSelector(node);
return root;
}
/*---------------------------------------------------------------------------------------------
* 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 {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';
suite('CSS - Code Actions', () => {
let testCodeActions = function (value: string, tokenBefore: string): Thenable<{ commands: Command[]; document: TextDocument; }> {
let document = TextDocument.create('test://test/test.css', 'css', 0, value);
let styleSheet = new Parser().parseStylesheet(document);
let offset = value.indexOf(tokenBefore);
let startPosition = document.positionAt(offset);
let endPosition = document.positionAt(offset + tokenBefore.length);
let range = Range.create(startPosition, endPosition);
let validation = new CSSValidation();
validation.configure({ validate: true });
return validation.doValidation(document, styleSheet).then(diagnostics => {
return new CSSCodeActions().doCodeActions(document, range, { diagnostics }, styleSheet).then(commands => {
return { commands, document };
});
});
};
let assertCodeAction = function (commands: Command[], document: TextDocument, expected: { title: string; content: string; }[]) {
let labels = commands.map(command => command.title);
for (let exp of expected) {
let index = labels.indexOf(exp.title);
assert.ok(index !== -1, 'Quick fix not found: ' + exp.title + ' , found:' + labels.join(','));
let command = commands[index];
assert.equal(applyEdits(document, <TextEdit[]>command.arguments[2]), exp.content);
assert.equal(command.arguments[0], document.uri);
assert.equal(command.arguments[1], document.version);
}
};
test('Unknown Properties', function (testDone): any {
Promise.all([
testCodeActions('body { /*here*/displai: inline }', '/*here*/').then((result) => {
assertCodeAction(result.commands, result.document, [
{ title: 'Rename to \'display\'', content: 'body { /*here*/display: inline }' }
])
}),
testCodeActions('body { /*here*/background-colar: red }', '/*here*/').then((result) => {
assertCodeAction(result.commands, result.document, [
{ title: 'Rename to \'background-color\'', content: 'body { /*here*/background-color: red }' },
{ title: 'Rename to \'background-clip\'', content: 'body { /*here*/background-clip: red }' },
{ title: 'Rename to \'background-image\'', content: 'body { /*here*/background-image: red }' }
])
})
]).then(() => testDone(), (error) => testDone(error));
});
})
/*---------------------------------------------------------------------------------------------
* 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 {CSSCompletion} from '../services/cssCompletion';
import {CompletionList, TextDocument, TextEdit, Position, CompletionItemKind} from 'vscode-languageserver';
import {applyEdits} from './textEditSupport';
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<CompletionList> {
let idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0;
let completionProvider = new CSSCompletion();
let document = TextDocument.create('test://test/test.css', 'css', 0, value);
let position = Position.create(0, idx);
let jsonDoc = new Parser().parseStylesheet(document);
let list = completionProvider.doComplete(document, position, jsonDoc);
if (expected.count) {
assert.equal(list.items, expected.count);
}
if (expected.items) {
for (let item of expected.items) {
assertCompletion(list, item, document);
}
}
return Promise.resolve(null);
};
test('sylesheet', function (testDone): any {
Promise.all([
testCompletionFor(' ', null, {
items: [
{ label: '@import' },
{ label: '@keyframes' },
{ label: 'div' }
]
}),
testCompletionFor(' body {', null, {
items: [
{ label: '@import' },
{ label: '@keyframes' },
{ label: 'html' }
]
}),
testCompletionFor('@import url("something.css");', '@', {
count: 0
})
]).then(() => testDone(), (error) => testDone(error));
});
test('properties', function (testDone): any {
Promise.all([
testCompletionFor('body {', '{', {
items: [
{ label: 'display' },
{ label: 'background' }
]
}),
testCompletionFor('body { ver', 'ver', {
items: [
{ label: 'vertical-align' }
]
}),
testCompletionFor('body { vertical-align', 'vertical-ali', {
items: [
{ label: 'vertical-align' }
]
}),
testCompletionFor('body { vertical-align', 'vertical-align', {
items: [
{ label: 'vertical-align' }
]
}),
testCompletionFor('body { vertical-align: bottom;}', 'vertical-align', {
items: [
{ label: 'vertical-align' }
]
}),
testCompletionFor('body { trans ', 'trans', {
items: [
{ label: 'transition' }
]
})
]).then(() => testDone(), (error) => testDone(error));
});
test('values', function (testDone): any {
Promise.all([
testCompletionFor('body { vertical-align: bottom;}', 'vertical-align:', {
items: [
{ label: 'bottom' },
{ label: '0cm' }
]
}),
testCompletionFor('body { vertical-align: bottom;}', 'vertical-align: ', {
items: [
{ label: 'bottom' },
{ label: '0cm' }
]
}),
testCompletionFor('body { vertical-align: bott', 'bott', {
items: [
{ label: 'bottom' }
]
}),
testCompletionFor('body { vertical-align: bottom }', 'bott', {
items: [
{ label: 'bottom' }
]
}),
testCompletionFor('body { vertical-align: bottom }', 'bottom', {
items: [
{ label: 'bottom' }
]
}),
testCompletionFor('body { vertical-align: bottom; }', 'bottom', {
items: [
{ label: 'bottom' }
]
}),
testCompletionFor('body { vertical-align: bottom; }', 'bottom;', {
count: 0
}),
testCompletionFor('body { vertical-align: bottom; }', 'bottom; ', {
items: [
{ label: 'display' }
]
})
]).then(() => testDone(), (error) => testDone(error));
});
test('units', function (testDone): any {
Promise.all([
testCompletionFor('body { vertical-align: 9 }', '9', {
items: [
{ label: '9cm' }
]
}),
testCompletionFor('body { vertical-align: 1.2 }', '1.2', {
items: [
{ label: '1.2em' }
]
}),
testCompletionFor('body { vertical-align: 10 }', '1', {
items: [
{ label: '1cm' }
]
}),
testCompletionFor('body { vertical-align: 10c }', '10c', {
items: [
{ label: '10cm' }
]
})
]).then(() => testDone(), (error) => testDone(error));
});
test('unknown', function (testDone): any {
Promise.all([
testCompletionFor('body { notexisting: ;}', 'notexisting: ', {
count: 0
}),
testCompletionFor('.foo { unknown: foo; } .bar { unknown: }', '.bar { unknown:', {
items: [
{ label: 'foo', kind: CompletionItemKind.Value }
]
})
]).then(() => testDone(), (error) => testDone(error));
});
test('colors', function (testDone): any {
Promise.all([
testCompletionFor('body { border-right: ', 'right: ', {
items: [
{ label: 'cyan' },
{ label: 'dotted' },
{ label: '0em' }
]
}),
testCompletionFor('body { border-right: cyan dotted 2em ', 'cyan', {
items: [
{ label: 'cyan' },
{ label: 'darkcyan' }
]
}),
testCompletionFor('body { border-right: dotted 2em ', '2em ', {
items: [
{ label: 'cyan' }
]
}),
testCompletionFor('.foo { background-color: #123456; } .bar { background-color: }', '.bar { background-color:', {
items: [
{ label: '#123456', kind: CompletionItemKind.Color }
]
}),
testCompletionFor('.foo { background-color: r', 'background-color: r', {
items: [
{ label: 'rgb', kind: CompletionItemKind.Function },
{ label: 'rgba', kind: CompletionItemKind.Function },
{ label: 'red', kind: CompletionItemKind.Color }
]
})
]).then(() => testDone(), (error) => testDone(error));
});
});
/*---------------------------------------------------------------------------------------------
* 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 * 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 {
let document = TextDocument.create('test://test/test.css', 'css', 0, text);
let stylesheet = parser.parseStylesheet(document);
assert.equal(0, nodes.ParseErrorCollector.entries(stylesheet).length, 'compile errors');
let node = nodes.getNodeAtOffset(stylesheet, text.indexOf(selection));
assert.equal(isColor, languageFacts.isColorValue(node));
}
suite('CSS - Language Facts', () => {
test('properties', function () {
let properties = languageFacts.getProperties();
let alignLast = properties['text-align-last'];
assert.ok(alignLast !== null);
assert.equal(alignLast.name, 'text-align-last');
let b = alignLast.browsers;
assert.equal(b['FF'], '12');
assert.equal(b['IE'], '5');
assert.equal(b['E'], '');
assert.equal(b['C'], void 0);
assert.equal(b['count'], 3);
assert.equal(languageFacts.getBrowserLabel(alignLast.browsers), 'Edge, Firefox 12, IE 5');
let r = alignLast.restrictions;
assert.equal(r.length, 1);
assert.equal(r[0], 'enum');
let v = alignLast.values;
assert.equal(v.length, 5);
assert.equal(v[0].name, 'auto');
assert.equal(v[0].browsers.all, true);
assert.equal(v[0].browsers.count, Number.MAX_VALUE);
});
test('is color', function () {
let parser = new Parser();
assertColor(parser, '#main { color: red }', 'red', true);
assertColor(parser, '#main { color: #231 }', '#231', true);
assertColor(parser, '#main { red: 1 }', 'red', false);
assertColor(parser, '#red { foo: 1 }', 'red', false);
});
});
/*---------------------------------------------------------------------------------------------
* 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 * 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 {
let visitor = new LintVisitor();
node.accept(visitor);
let entries = visitor.getEntries(nodes.Level.Error | nodes.Level.Warning | nodes.Level.Ignore);
assert.equal(entries.length, rules.length);
for (let entry of entries) {
let idx = rules.indexOf(entry.getRule());
rules.splice(idx, 1);
}
assert.equal(rules.length, 0);
}
function assertStyleSheet(input: string, ...rules: Rule[]): void {
let p = new Parser();
let document = TextDocument.create('test://test/test.css', 'css', 0, input);
let node = p.parseStylesheet(document);
assertEntries(node, rules);
}
function assertRuleSet(input: string, ...rules: Rule[]): void {
let p = new Parser();
let node = p.internalParse(input, p._parseRuleset);
assertEntries(node, rules);
}
function assertFontFace(input: string, ...rules: Rule[]): void {
let p = new Parser();
let node = p.internalParse(input, p._parseFontFace);
assertEntries(node, rules);
}
suite('CSS - Lint', () => {
test('universal selector, empty rule', function () {
assertRuleSet('* { color: perty }', Rules.UniversalSelector);
assertRuleSet('*, div { color: perty }', Rules.UniversalSelector);
assertRuleSet('div, * { color: perty }', Rules.UniversalSelector);
assertRuleSet('div > * { color: perty }', Rules.UniversalSelector);
assertRuleSet('div + * { color: perty }', Rules.UniversalSelector);
});
test('empty ruleset', function () {
assertRuleSet('selector {}', Rules.EmptyRuleSet);
});
test('properies ignored due to inline ', function () {
assertRuleSet('selector { display: inline; height: 100px; }', Rules.PropertyIgnoredDueToDisplay);
assertRuleSet('selector { display: inline; width: 100px; }', Rules.PropertyIgnoredDueToDisplay);
assertRuleSet('selector { display: inline; margin-top: 1em; }', Rules.PropertyIgnoredDueToDisplay);
assertRuleSet('selector { display: inline; margin-bottom: 1em; }', Rules.PropertyIgnoredDueToDisplay);
assertRuleSet('selector { display: inline; float: right; }', Rules.PropertyIgnoredDueToDisplay, Rules.AvoidFloat);
assertRuleSet('selector { display: inline-block; float: right; }', Rules.PropertyIgnoredDueToDisplay, Rules.AvoidFloat);
assertRuleSet('selector { display: block; vertical-align: center; }', Rules.PropertyIgnoredDueToDisplay);
});
test('avoid !important', function () {
assertRuleSet('selector { display: inline !important; }', Rules.AvoidImportant);
});
test('avoid float', function () {
assertRuleSet('selector { float: right; }', Rules.AvoidFloat);
});
test('avoid id selectors', function () {
assertRuleSet('#selector { display: inline; }', Rules.AvoidIdSelector);
});
test('zero with unit', function () {
// assertRuleSet('selector { width: 0px }', lint.Rules.ZeroWithUnit);
assertRuleSet('selector { width: 0% }');
});
test('duplicate declarations', function () {
assertRuleSet('selector { color: perty; color: perty }', Rules.DuplicateDeclarations, Rules.DuplicateDeclarations);
assertRuleSet('selector { color: -o-perty; color: perty }');
});
test('unknown properties', function () {
assertRuleSet('selector { -ms-property: "rest is missing" }', Rules.UnknownVendorSpecificProperty);
assertRuleSet('selector { -moz-box-shadow: "rest is missing" }', Rules.UnknownVendorSpecificProperty, Rules.IncludeStandardPropertyWhenUsingVendorPrefix);
assertRuleSet('selector { box-shadow: none }'); // no error
assertRuleSet('selector { box-property: "rest is missing" }', Rules.UnknownProperty);
});
test('IE hacks', function () {
assertRuleSet('selector { display: inline-block; *display: inline; }', Rules.IEStarHack);
assertRuleSet('selector { background: #00f; /* all browsers including Mac IE */ *background: #f00; /* IE 7 and below */ _background: #f60; /* IE 6 and below */ }', Rules.IEStarHack, Rules.IEStarHack);
});
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 { transform: none; }');
assertRuleSet('selector { -moz-transform: none; transform: none; -o-transform: none; -webkit-transform: none; -ms-transform: none; }');
assertRuleSet('selector { --transform: none; }');
});
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 { src: url(test.tff); font-family: \'name\' }'); // no error
});
test('keyframes', function () {
assertStyleSheet('@keyframes foo { }');
assertStyleSheet('@keyframes foo { } @-moz-keyframes foo { }', Rules.AllVendorPrefixes);
assertStyleSheet('@-moz-keyframes foo { }', Rules.AllVendorPrefixes, Rules.IncludeStandardPropertyWhenUsingVendorPrefix);
});
});
/*---------------------------------------------------------------------------------------------
* 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 {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';
export function assertScopesAndSymbols(p: Parser, input: string, expected: string): void {
let global = createScope(p, input);
assert.equal(scopeToString(global), expected);
}
export function assertHighlights(p: Parser, input: string, marker: string, expectedMatches: number, expectedWrites: number): void {
let document = TextDocument.create('test://test/test.css', 'css', 0, input);
let stylesheet = p.parseStylesheet(document);
assertNoErrors(stylesheet);
let index = input.indexOf(marker) + marker.length;
let position = document.positionAt(index);
let highlights = new CSSNavigation().findDocumentHighlights(document, position, stylesheet);
assert.equal(highlights.length, expectedMatches);
let nWrites = 0;
for (let highlight of highlights) {
if (highlight.kind === DocumentHighlightKind.Write) {
nWrites++;
}
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(nWrites, expectedWrites);
}
export function assertSymbolsInScope(p: Parser, input: string, offset: number, ...selections: { name: string; type: nodes.ReferenceType }[]): void {
let global = createScope(p, input);
let scope = global.findScope(offset);
let getErrorMessage = function (name: string) {
let all = 'symbol ' + name + ' not found. In scope: ';
scope.getSymbols().forEach((sym) => { all += (sym.name + ' '); });
return all;
};
for (let i = 0; i < selections.length; i++) {
let selection = selections[i];
let sym = scope.getSymbol(selection.name, selection.type) || global.getSymbol(selection.name, selection.type);
assert.ok(!!sym, getErrorMessage(selection.name));
}
}
export function assertScopeBuilding(p: Parser, input: string, ...scopes: { offset: number; length: number; }[]): void {
let global = createScope(p, input);
function assertChildren(scope: Scope): void {
scope.children.forEach((scope) => {
// check bounds
let expected = scopes.shift();
assert.equal(scope.offset, expected.offset);
assert.equal(scope.length, expected.length);
// recursive descent
assertChildren(scope);
});
}
assertChildren(global);
assert.equal(scopes.length, 0, 'remainig scopes: ' + scopes.join());
}
function scopeToString(scope: Scope): string {
let str = '';
let symbols = scope.getSymbols();
for (let index = 0; index < symbols.length; index++) {
if (str.length > 0) {
str += ',';
}
str += symbols[index].name;
}
let scopes = scope.children;
for (let index = 0; index < scopes.length; index++) {
if (str.length > 0) {
str += ',';
}
str += ('[' + scopeToString(scopes[index]) + ']');
}
return str;
}
function assertNoErrors(node: nodes.Node): void {
let markers = nodes.ParseErrorCollector.entries(node);
if (markers.length > 0) {
assert.ok(false, 'node has errors: ' + markers[0].getMessage() + ', offset: ' + markers[0].getNode().offset);
}
}
function createScope(p: Parser, input: string): Scope {
let document = TextDocument.create('test://test/test.css', 'css', 0, input);
let styleSheet = p.parseStylesheet(document),
global = new GlobalScope(),
builder = new ScopeBuilder(global);
assertNoErrors(styleSheet);
styleSheet.accept(builder);
return global;
}
suite('CSS - Symbols', () => {
test('scope creation', function () {
let global = new GlobalScope(),
child1 = new Scope(10, 5),
child2 = new Scope(15, 5);
global.addChild(child1);
global.addChild(child2);
assert.equal(global.children.length, 2);
assert.ok(child1.parent === global);
assert.ok(child2.parent === global);
// find children
assert.ok(global.findScope(-1) === null);
assert.ok(global.findScope(0) === global);
assert.ok(global.findScope(10) === child1);
assert.ok(global.findScope(14) === child1);
assert.ok(global.findScope(15) === child2);
assert.ok(global.findScope(19) === child2);
assert.ok(global.findScope(19).parent === global);
});
test('scope building', function () {
let p = new Parser();
assertScopeBuilding(p, '.class {}', { offset: 7, length: 2 });
assertScopeBuilding(p, '.class {} .class {}', { offset: 7, length: 2 }, { offset: 17, length: 2 });
});
test('symbols in scopes', function () {
let p = new Parser();
assertSymbolsInScope(p, '@keyframes animation {};', 0, { name: 'animation', type: nodes.ReferenceType.Keyframe });
assertSymbolsInScope(p, ' .class1 {} .class2 {}', 0, { name: '.class1', type: nodes.ReferenceType.Rule }, { name: '.class2', type: nodes.ReferenceType.Rule });
});
test('scopes and symbols', function () {
let p = new Parser();
assertScopesAndSymbols(p, '.class {}', '.class,[]');
assertScopesAndSymbols(p, '@keyframes animation {}; .class {}', 'animation,.class,[],[]');
assertScopesAndSymbols(p, '@page :pseudo-class { margin:2in; }', '[]');
assertScopesAndSymbols(p, '@media print { body { font-size: 10pt } }', '[body,[]]');
assertScopesAndSymbols(p, '@-moz-keyframes identifier { 0% { top: 0; } 50% { top: 30px; left: 20px; }}', 'identifier,[[],[]]');
assertScopesAndSymbols(p, '@font-face { font-family: "Bitstream Vera Serif Bold"; }', '[]');
});
test('mark occurrences', function () {
let p = new Parser();
assertHighlights(p, '@keyframes id {}; #main { animation: id 4s linear 0s infinite alternate; }', 'id', 2, 1);
assertHighlights(p, '@keyframes id {}; #main { animation-name: id; foo: id;}', 'id', 2, 1);
});
test('test variables in root scope', function () {
let p = new Parser();
assertSymbolsInScope(p, ':root{ --var1: abc; --var2: def; }', 0, { name: '--var1', type: nodes.ReferenceType.Variable }, { name: '--var2', type: nodes.ReferenceType.Variable });
});
test('test variables in local scope', function () {
let p = new Parser();
assertSymbolsInScope(p, '.a{ --var1: abc; --var2: def; }', 2, { name: '--var1', type: nodes.ReferenceType.Variable }, { name: '--var2', type: nodes.ReferenceType.Variable });
});
test('test variables in local scope get root variables too', function () {
let p = new Parser();
assertSymbolsInScope(p, '.a{ --var1: abc; } :root{ --var2: abc;}', 2, { name: '--var1', type: nodes.ReferenceType.Variable }, { name: '--var2', type: nodes.ReferenceType.Variable });
});
test('test variables in local scope get root variables and other local variables too', function () {
let p = new Parser();
assertSymbolsInScope(p, '.a{ --var1: abc; } .b{ --var2: abc; } :root{ --var3: abc;}', 2, { name: '--var1', type: nodes.ReferenceType.Variable }, { name: '--var2', type: nodes.ReferenceType.Variable }, { name: '--var3', type: nodes.ReferenceType.Variable });
});
test('mark occurrences for variable defined in root and used in a rule', function () {
let p = new Parser();
assertHighlights(p, '.a{ background: let(--var1); } :root{ --var1: abc;}', '--var1', 2, 1);
});
test('mark occurrences for variable defined in a rule and used in a different rule', function () {
let p = new Parser();
assertHighlights(p, '.a{ background: let(--var1); } :b{ --var1: abc;}', '--var1', 2, 1);
});
test('mark occurrences for property', function () {
let p = new Parser();
assertHighlights(p, 'body { display: inline } #foo { display: inline }', 'display', 2, 0);
});
test('mark occurrences for value', function () {
let p = new Parser();
assertHighlights(p, 'body { display: inline } #foo { display: inline }', 'inline', 2, 0);
});
test('mark occurrences for selector', function () {
let p = new Parser();
assertHighlights(p, 'body { display: inline } #foo { display: inline }', 'body', 1, 1);
});
test('mark occurrences for comment', function () {
let p = new Parser();
assertHighlights(p, '/* comment */body { display: inline } ', 'comment', 0, 0);
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 * as nodes from '../parser/cssNodes';
import {Parser} from '../parser/cssParser';
export class PrintingVisitor implements nodes.IVisitor {
public tree: string[] = [];
public visitNode(node: nodes.Node): boolean {
this.tree.push(nodes.NodeType[node.type].toLowerCase());
return true;
}
}
export function assertNodes(fn: (input: string) => nodes.Node, input: string, expected: string): void {
let node = fn(input);
let visitor = new PrintingVisitor();
node.accept(visitor);
let actual = visitor.tree.join(',') + ',';
let segments = expected.split(',');
let oldIndex: number = undefined;
let index = -1;
while (segments.length > 0) {
let segment = segments.shift();
if (segment === '...') {
continue;
}
index = actual.indexOf(segment + ',', oldIndex);
if (index <= oldIndex) {
assert.ok(false, segment + ' NOT found in ' + actual);
}
oldIndex = index + segment.length;
}
assert.ok(true);
}
suite('CSS - Nodes', () => {
test('Test Node', function () {
let node = new nodes.Node();
assert.equal(node.offset, -1);
assert.equal(node.length, -1);
assert.equal(node.parent, null);
assert.equal(node.getChildren().length, 0);
let c = 0;
node.accept((n: nodes.Node) => {
assert.ok(n === node);
c += 1;
return true;
});
assert.equal(c, 1);
let child = new nodes.Node();
node.adoptChild(child);
c = 0;
let expects = [node, child];
node.accept((n: nodes.Node) => {
assert.ok(n === expects[c]);
c += 1;
return true;
});
assert.equal(c, 2);
});
test('Test Adopting', function () {
let child = new nodes.Node();
let p1 = new nodes.Node();
let p2 = new nodes.Node();
assert.ok(child.parent === null);
assert.equal(p1.getChildren().length, 0);
assert.equal(p2.getChildren().length, 0);
p1.adoptChild(child);
assert.ok(child.parent === p1);
assert.equal(p1.getChildren().length, 1);
assert.equal(p2.getChildren().length, 0);
p2.adoptChild(child);
assert.ok(child.parent === p2);
assert.equal(p1.getChildren().length, 0);
assert.equal(p2.getChildren().length, 1);
});
function ruleset(input: string): nodes.RuleSet {
let parser = new Parser();
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');
});
test('Keyframe', function () {
function fn(input: string): nodes.Node {
let parser = new Parser();
let node = parser.internalParse(input, parser._parseKeyframe);
return node;
};
assertNodes(fn, '@keyframes name { from { top: 0px} to { top: 100px } }', 'keyframe,identifier,keyframeselector,declaration,keyframeselector,declaration');
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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';
export function assertNode(text: string, parser: Parser, f: () => nodes.Node): nodes.Node {
let node = parser.internalParse(text, f);
assert.ok(node !== null, 'no node returned');
let markers = nodes.ParseErrorCollector.entries(node);
if (markers.length > 0) {
assert.ok(false, 'node has errors: ' + markers[0].getMessage() + ', offset: ' + markers[0].getNode().offset);
}
assert.ok(parser.accept(TokenType.EOF), 'Expect scanner at EOF');
return node;
}
export function assertFunction(text: string, parser: Parser, f: () => nodes.Node): void {
assertNode(text, parser, f);
}
export function assertNoNode(text: string, parser: Parser, f: () => nodes.Node): void {
let node = parser.internalParse(text, f);
assert.ok(node === null);
}
export function assertError(text: string, parser: Parser, f: () => nodes.Node, error: nodes.IRule): void {
let node = parser.internalParse(text, f);
assert.ok(node !== null, 'no node returned');
let markers = nodes.ParseErrorCollector.entries(node);
if (markers.length === 0) {
assert.ok(false, 'no errors but error expected: ' + error.message);
} else {
markers = markers.sort((a, b) => { return a.getOffset() - b.getOffset(); });
assert.equal(markers[0].getRule().id, error.id);
}
}
suite('CSS - Parser', () => {
test('Test stylesheet', function () {
let parser = new Parser();
assertNode('@charset "demo" ;', parser, parser._parseStylesheet.bind(parser));
assertNode('body { margin: 0px; padding: 3em, 6em; }', parser, parser._parseStylesheet.bind(parser));
assertNode('--> <!--', parser, parser._parseStylesheet.bind(parser));
assertNode('', parser, parser._parseStylesheet.bind(parser));
assertNode('<!-- --> @import "string"; <!-- -->', parser, parser._parseStylesheet.bind(parser));
assertNode('@media asdsa { } <!-- --> <!-- -->', parser, parser._parseStylesheet.bind(parser));
assertNode('@media screen, projection { }', parser, parser._parseStylesheet.bind(parser));
assertNode('@media screen and (max-width: 400px) { @-ms-viewport { width: 320px; }}', parser, parser._parseStylesheet.bind(parser));
assertNode('@-ms-viewport { width: 320px; height: 768px; }', parser, parser._parseStylesheet.bind(parser));
assertNode('#boo, far {} \n.far boo {}', parser, parser._parseStylesheet.bind(parser));
assertNode('@-moz-keyframes darkWordHighlight { from { background-color: inherit; } to { background-color: rgba(83, 83, 83, 0.7); } }', parser, parser._parseStylesheet.bind(parser));
assertNode('@import "foo";', parser, parser._parseStylesheet.bind(parser));
assertNode('@import url(/css/screen.css) screen, projection;', parser, parser._parseStylesheet.bind(parser));
assertNode('@page { margin: 2.5cm; }', parser, parser._parseStylesheet.bind(parser));
assertNode('@font-face { font-family: "Example Font"; }', parser, parser._parseStylesheet.bind(parser));
assertNode('@namespace "http://www.w3.org/1999/xhtml";', parser, parser._parseStylesheet.bind(parser));
assertNode('@namespace pref url(http://test);', parser, parser._parseStylesheet.bind(parser));
assertNode('@-moz-document url(http://test), url-prefix(http://www.w3.org/Style/) { body { color: purple; background: yellow; } }', parser, parser._parseStylesheet.bind(parser));
assertNode('E E[foo] E[foo="bar"] E[foo~="bar"] E[foo^="bar"] E[foo$="bar"] E[foo*="bar"] E[foo|="en"] {}', parser, parser._parseStylesheet.bind(parser));
assertNode('input[type=\"submit\"] {}', parser, parser._parseStylesheet.bind(parser));
assertNode('E:root E:nth-child(n) E:nth-last-child(n) E:nth-of-type(n) E:nth-last-of-type(n) E:first-child E:last-child {}', parser, parser._parseStylesheet.bind(parser));
assertNode('E:first-of-type E:last-of-type E:only-child E:only-of-type E:empty E:link E:visited E:active E:hover E:focus E:target E:lang(fr) E:enabled E:disabled E:checked {}', parser, parser._parseStylesheet.bind(parser));
assertNode('E::first-line E::first-letter E::before E::after {}', parser, parser._parseStylesheet.bind(parser));
assertNode('E.warning E#myid E:not(s) {}', parser, parser._parseStylesheet.bind(parser));
assertError('@namespace;', parser, parser._parseStylesheet.bind(parser), ParseError.URIExpected);
assertError('@namespace url(http://test)', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
assertError('@mskeyframes darkWordHighlight { from { background-color: inherit; } to { background-color: rgba(83, 83, 83, 0.7); } }', parser, parser._parseStylesheet.bind(parser), ParseError.UnknownAtRule);
assertError('@charset;', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
assertError('@charset \'utf8\'', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected);
});
test('Stylesheet /Panic/', function () {
let parser = new Parser();
assertError('#boo, far } \n.far boo {}', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected);
assertError('#boo, far { far: 43px; \n.far boo {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected);
});
test('@font-face', function () {
let parser = new Parser();
assertNode('@font-face {}', parser, parser._parseFontFace.bind(parser));
assertNode('@font-face { src: url(http://test) }', parser, parser._parseFontFace.bind(parser));
assertNode('@font-face { font-style: normal; font-stretch: normal; }', parser, parser._parseFontFace.bind(parser));
assertError('@font-face { font-style: normal font-stretch: normal; }', parser, parser._parseFontFace.bind(parser), ParseError.SemiColonExpected);
});
test('@keyframe selector', function () {
let parser = new Parser();
assertNode('from {}', parser, parser._parseKeyframeSelector.bind(parser));
assertNode('to {}', parser, parser._parseKeyframeSelector.bind(parser));
assertNode('0% {}', parser, parser._parseKeyframeSelector.bind(parser));
assertNode('10% {}', parser, parser._parseKeyframeSelector.bind(parser));
assertNode('100000% {}', parser, parser._parseKeyframeSelector.bind(parser));
assertNode('from { width: 100% }', parser, parser._parseKeyframeSelector.bind(parser));
assertNode('from { width: 100%; to: 10px; }', parser, parser._parseKeyframeSelector.bind(parser));
});
test('@keyframe', function () {
let parser = new Parser();
assertNode('@keyframes name {}', parser, parser._parseKeyframe.bind(parser));
assertNode('@-webkit-keyframes name {}', parser, parser._parseKeyframe.bind(parser));
assertNode('@-o-keyframes name {}', parser, parser._parseKeyframe.bind(parser));
assertNode('@-moz-keyframes name {}', parser, parser._parseKeyframe.bind(parser));
assertNode('@keyframes name { from {} to {}}', parser, parser._parseKeyframe.bind(parser));
assertNode('@keyframes name { from {} 80% {} 100% {}}', parser, parser._parseKeyframe.bind(parser));
assertNode('@keyframes name { from { top: 0px; } 80% { top: 100px; } 100% { top: 50px; }}', parser, parser._parseKeyframe.bind(parser));
assertNode('@keyframes name { from { top: 0px; } 70%, 80% { top: 100px; } 100% { top: 50px; }}', parser, parser._parseKeyframe.bind(parser));
assertNode('@keyframes name { from { top: 0px; left: 1px; right: 2px }}', parser, parser._parseKeyframe.bind(parser));
assertError('@keyframes name { from { top: 0px; left: 1px, right: 2px }}', parser, parser._parseKeyframe.bind(parser), ParseError.SemiColonExpected);
assertError('@keyframes )', parser, parser._parseKeyframe.bind(parser), ParseError.IdentifierExpected);
assertError('@keyframes name { { top: 0px; } }', parser, parser._parseKeyframe.bind(parser), ParseError.RightCurlyExpected);
assertError('@keyframes name { from, #123', parser, parser._parseKeyframe.bind(parser), ParseError.PercentageExpected);
});
test('Test import', function () {
let parser = new Parser();
assertNode('@import "asdasdsa"', parser, parser._parseImport.bind(parser));
assertNode('@ImPort "asdsadsa"', parser, parser._parseImport.bind(parser));
assertNode('@import "asdasd" dsfsdf', parser, parser._parseImport.bind(parser));
assertError('@import', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected);
});
test('Test media', function () {
let parser = new Parser();
assertNode('@media asdsa { }', parser, parser._parseMedia.bind(parser));
assertNode('@meDia sadd{} ', parser, parser._parseMedia.bind(parser));
assertNode('@media somename, othername2 { }', parser, parser._parseMedia.bind(parser));
assertNode('@media only screen and (max-width:850px) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media only screen and (max-width:850px) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media all and (min-width:500px) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media screen and (color), projection and (color) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media not screen and (device-aspect-ratio: 16/9) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media print and (min-resolution: 300dpi) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media print and (min-resolution: 118dpcm) { }', parser, parser._parseMedia.bind(parser));
assertNode('@media print { @page { margin: 10% } blockquote, pre { page-break-inside: avoid } }', parser, parser._parseMedia.bind(parser));
assertNode('@media print { body:before { } }', parser, parser._parseMedia.bind(parser));
assertError('@media somename othername2 { }', parser, parser._parseMedia.bind(parser), ParseError.LeftCurlyExpected);
assertError('@media not, screen { }', parser, parser._parseMedia.bind(parser), ParseError.IdentifierExpected);
assertError('@media not screen and foo { }', parser, parser._parseMedia.bind(parser), ParseError.LeftParenthesisExpected);
assertError('@media not screen and () { }', parser, parser._parseMedia.bind(parser), ParseError.IdentifierExpected);
assertError('@media not screen and (color:) { }', parser, parser._parseMedia.bind(parser), ParseError.TermExpected);
assertError('@media not screen and (color:#234567 { }', parser, parser._parseMedia.bind(parser), ParseError.RightParenthesisExpected);
});
test('Test media_list', function () {
let parser = new Parser();
assertNode('somename', parser, parser._parseMediaList.bind(parser));
assertNode('somename, othername', parser, parser._parseMediaList.bind(parser));
});
test('medium', function () {
let parser = new Parser();
assertNode('somename', parser, parser._parseMedium.bind(parser));
assertNode('-asdas', parser, parser._parseMedium.bind(parser));
assertNode('-asda34s', parser, parser._parseMedium.bind(parser));
});
test('page', function () {
let parser = new Parser();
assertNode('@page : name{ }', parser, parser._parsePage.bind(parser));
assertNode('@page :left, :right { }', parser, parser._parsePage.bind(parser));
assertNode('@page : name{ some : "asdas" }', parser, parser._parsePage.bind(parser));
assertNode('@page : name{ some : "asdas" !important }', parser, parser._parsePage.bind(parser));
assertNode('@page : name{ some : "asdas" !important; some : "asdas" !important }', parser, parser._parsePage.bind(parser));
assertNode('@page rotated { size : landscape }', parser, parser._parsePage.bind(parser));
assertNode('@page :left { margin-left: 4cm; margin-right: 3cm; }', parser, parser._parsePage.bind(parser));
assertNode('@page { @top-right-corner { content: url(foo.png); border: solid green; } }', parser, parser._parsePage.bind(parser));
assertNode('@page { @top-left-corner { content: " "; border: solid green; } @bottom-right-corner { content: counter(page); border: solid green; } }', parser, parser._parsePage.bind(parser));
assertError('@page { @top-left-corner foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.LeftCurlyExpected);
assertError('@page { @XY foo { content: " "; border: solid green; } }', parser, parser._parsePage.bind(parser), ParseError.UnknownAtRule);
assertError('@page :left { margin-left: 4cm margin-right: 3cm; }', parser, parser._parsePage.bind(parser), ParseError.SemiColonExpected);
assertError('@page : { }', parser, parser._parsePage.bind(parser), ParseError.IdentifierExpected);
assertError('@page :left, { }', parser, parser._parsePage.bind(parser), ParseError.IdentifierExpected);
});
test('pseudo page', function () {
let parser = new Parser();
assertNode(': some ', parser, parser._parsePageSelector.bind(parser));
});
test('operator', function () {
let parser = new 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));
});
test('combinator', function () {
let parser = new Parser();
assertNode('+', parser, parser._parseCombinator.bind(parser));
assertNode('+ ', parser, parser._parseCombinator.bind(parser));
assertNode('> ', parser, parser._parseCombinator.bind(parser));
assertNode('>', parser, parser._parseCombinator.bind(parser));
});
test('unary_operator', function () {
let parser = new Parser();
assertNode('-', parser, parser._parseUnaryOperator.bind(parser));
assertNode('+', parser, parser._parseUnaryOperator.bind(parser));
});
test('Property', function () {
let parser = new Parser();
assertNode('asdsa', parser, parser._parseProperty.bind(parser));
assertNode('asdsa334', parser, parser._parseProperty.bind(parser));
assertNode('--color', parser, parser._parseProperty.bind(parser));
assertNode('--primary-font', parser, parser._parseProperty.bind(parser));
assertNode('-color', parser, parser._parseProperty.bind(parser));
assertNode('somevar', parser, parser._parseProperty.bind(parser));
assertNode('some--let', parser, parser._parseProperty.bind(parser));
assertNode('somevar--', parser, parser._parseProperty.bind(parser));
});
test('Ruleset', function () {
let parser = new Parser();
assertNode('name{ }', parser, parser._parseRuleset.bind(parser));
assertNode(' name\n{ some : "asdas" }', parser, parser._parseRuleset.bind(parser));
assertNode(' name{ some : "asdas" !important }', parser, parser._parseRuleset.bind(parser));
assertNode('name{ \n some : "asdas" !important; some : "asdas" }', parser, parser._parseRuleset.bind(parser));
assertNode('* {}', parser, parser._parseRuleset.bind(parser));
assertNode('.far{}', parser, parser._parseRuleset.bind(parser));
assertNode('boo {}', parser, parser._parseRuleset.bind(parser));
assertNode('.far #boo {}', parser, parser._parseRuleset.bind(parser));
assertNode('boo { prop: value }', parser, parser._parseRuleset.bind(parser));
assertNode('boo { prop: value; }', parser, parser._parseRuleset.bind(parser));
assertNode('boo { prop: value; prop: value }', parser, parser._parseRuleset.bind(parser));
assertNode('boo { prop: value; prop: value; }', parser, parser._parseRuleset.bind(parser));
});
test('Ruleset /Panic/', function () {
let parser = new Parser();
// assertNode('boo { : value }', parser, parser._parseRuleset.bind(parser));
assertError('boo { prop: ; }', parser, parser._parseRuleset.bind(parser), ParseError.PropertyValueExpected);
assertError('boo { prop }', parser, parser._parseRuleset.bind(parser), ParseError.ColonExpected);
assertError('boo { prop: ; far: 12em; }', parser, parser._parseRuleset.bind(parser), ParseError.PropertyValueExpected);
// assertNode('boo { prop: ; 1ar: 12em; }', parser, parser._parseRuleset.bind(parser));
});
test('selector', function () {
let parser = new Parser();
assertNode('asdsa', parser, parser._parseSelector.bind(parser));
assertNode('asdsa + asdas', parser, parser._parseSelector.bind(parser));
assertNode('asdsa + asdas + name', parser, parser._parseSelector.bind(parser));
assertNode('asdsa + asdas + name', parser, parser._parseSelector.bind(parser));
assertNode('name #id#anotherid', parser, parser._parseSelector.bind(parser));
assertNode('name.far .boo', parser, parser._parseSelector.bind(parser));
assertNode('name .name .zweitername', parser, parser._parseSelector.bind(parser));
assertNode('*', parser, parser._parseSelector.bind(parser));
assertNode('#id', parser, parser._parseSelector.bind(parser));
assertNode('far.boo', parser, parser._parseSelector.bind(parser));
});
test('simple selector', function () {
let parser = new Parser();
assertNode('name', parser, parser._parseSimpleSelector.bind(parser));
assertNode('#id#anotherid', parser, parser._parseSimpleSelector.bind(parser));
assertNode('name.far', parser, parser._parseSimpleSelector.bind(parser));
assertNode('name.erstername.zweitername', parser, parser._parseSimpleSelector.bind(parser));
});
test('element name', function () {
let parser = new Parser();
assertNode('name', parser, parser._parseElementName.bind(parser));
assertNode('*', parser, parser._parseElementName.bind(parser));
});
test('attrib', function () {
let parser = new Parser();
assertNode('[name]', parser, parser._parseAttrib.bind(parser));
assertNode('[name = name2]', parser, parser._parseAttrib.bind(parser));
assertNode('[name ~= name3]', parser, parser._parseAttrib.bind(parser));
assertNode('[name~=name3]', parser, parser._parseAttrib.bind(parser));
assertNode('[name |= name3]', parser, parser._parseAttrib.bind(parser));
assertNode('[name |= "this is a striiiing"]', parser, parser._parseAttrib.bind(parser));
});
test('pseudo', function () {
let parser = new Parser();
assertNode(':some', parser, parser._parsePseudo.bind(parser));
assertNode(':some(thing)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(12)', parser, parser._parsePseudo.bind(parser));
assertNode(':lang(it)', parser, parser._parsePseudo.bind(parser));
assertNode(':not(.class)', parser, parser._parsePseudo.bind(parser));
assertNode(':not(:disabled)', parser, parser._parsePseudo.bind(parser));
assertNode(':not(#foo)', parser, parser._parsePseudo.bind(parser));
});
test('declaration', function () {
let parser = new Parser();
assertNode('name : "this is a string" !important', parser, parser._parseDeclaration.bind(parser));
assertNode('name : "this is a string"', parser, parser._parseDeclaration.bind(parser));
assertNode('property:12', parser, parser._parseDeclaration.bind(parser));
assertNode('-vendor-property: 12', parser, parser._parseDeclaration.bind(parser));
assertNode('font-size: 12px', parser, parser._parseDeclaration.bind(parser));
assertNode('color : #888 /4', parser, parser._parseDeclaration.bind(parser));
assertNode('filter : progid:DXImageTransform.Microsoft.Shadow(color=#000000,direction=45)', parser, parser._parseDeclaration.bind(parser));
assertNode('filter : progid: DXImageTransform.\nMicrosoft.\nDropShadow(\noffx=2, offy=1, color=#000000)', parser, parser._parseDeclaration.bind(parser));
assertNode('font-size: 12px', parser, parser._parseDeclaration.bind(parser));
assertNode('*background: #f00 /* IE 7 and below */', parser, parser._parseDeclaration.bind(parser));
assertNode('_background: #f60 /* IE 6 and below */', parser, parser._parseDeclaration.bind(parser));
assertNode('background-image: linear-gradient(to right, silver, white 50px, white calc(100% - 50px), silver)', parser, parser._parseDeclaration.bind(parser));
assertNode('--color: #F5F5F5', parser, parser._parseDeclaration.bind(parser));
assertNode('--color: 0', parser, parser._parseDeclaration.bind(parser));
assertNode('--color: 255', parser, parser._parseDeclaration.bind(parser));
assertNode('--color: 25.5', parser, parser._parseDeclaration.bind(parser));
assertNode('--color: 25px', parser, parser._parseDeclaration.bind(parser));
assertNode('--color: 25.5px', parser, parser._parseDeclaration.bind(parser));
assertNode('--primary-font: "wf_SegoeUI","Segoe UI","Segoe","Segoe WP"', parser, parser._parseDeclaration.bind(parser));
assertError('--color : ', parser, parser._parseDeclaration.bind(parser), ParseError.PropertyValueExpected);
assertError('--color value', parser, parser._parseDeclaration.bind(parser), ParseError.ColonExpected);
});
test('term', function () {
let parser = new Parser();
assertNode('"asdasd"', parser, parser._parseTerm.bind(parser));
assertNode('name', parser, parser._parseTerm.bind(parser));
assertNode('#FFFFFF', parser, parser._parseTerm.bind(parser));
assertNode('url("this is a url")', parser, parser._parseTerm.bind(parser));
assertNode('+324', parser, parser._parseTerm.bind(parser));
assertNode('-45', parser, parser._parseTerm.bind(parser));
assertNode('+45', parser, parser._parseTerm.bind(parser));
assertNode('-45%', parser, parser._parseTerm.bind(parser));
assertNode('-45mm', parser, parser._parseTerm.bind(parser));
assertNode('-45em', parser, parser._parseTerm.bind(parser));
assertNode('"asdsa"', parser, parser._parseTerm.bind(parser));
assertNode('faa', parser, parser._parseTerm.bind(parser));
assertNode('url("this is a striiiiing")', parser, parser._parseTerm.bind(parser));
assertNode('#FFFFFF', parser, parser._parseTerm.bind(parser));
assertNode('name(asd)', parser, parser._parseTerm.bind(parser));
assertNode('calc(50% + 20px)', parser, parser._parseTerm.bind(parser));
assertNode('calc(50% + (100%/3 - 2*1em - 2*1px))', parser, parser._parseTerm.bind(parser));
assertNoNode('%(\'repetitions: %S file: %S\', 1 + 2, "directory/file.less")', parser, parser._parseTerm.bind(parser)); // less syntax
assertNoNode('~"ms:alwaysHasItsOwnSyntax.For.Stuff()"', parser, parser._parseTerm.bind(parser)); // less syntax
});
test('function', function () {
let parser = new Parser();
assertNode('name( "bla" )', parser, parser._parseFunction.bind(parser));
assertNode('name( name )', parser, parser._parseFunction.bind(parser));
assertNode('name( -500mm )', parser, parser._parseFunction.bind(parser));
assertNode('\u060frf()', parser, parser._parseFunction.bind(parser));
assertNode('über()', parser, parser._parseFunction.bind(parser));
assertNoNode('über ()', parser, parser._parseFunction.bind(parser));
assertNoNode('%()', parser, parser._parseFunction.bind(parser));
assertNoNode('% ()', parser, parser._parseFunction.bind(parser));
assertFunction('let(--color)', parser, parser._parseFunction.bind(parser));
assertFunction('let(--color, somevalue)', parser, parser._parseFunction.bind(parser));
assertFunction('let(--variable1, --variable2)', parser, parser._parseFunction.bind(parser));
assertFunction('let(--variable1, let(--variable2))', parser, parser._parseFunction.bind(parser));
assertFunction('fun(value1, value2)', parser, parser._parseFunction.bind(parser));
});
test('Test Token prio', function () {
let parser = new Parser();
assertNode('!important', parser, parser._parsePrio.bind(parser));
assertNode('!/*demo*/important', parser, parser._parsePrio.bind(parser));
assertNode('! /*demo*/ important', parser, parser._parsePrio.bind(parser));
assertNode('! /*dem o*/ important', parser, parser._parsePrio.bind(parser));
});
test('hexcolor', function () {
let parser = new Parser();
assertNode('#FFF', parser, parser._parseHexColor.bind(parser));
assertNode('#FFFFFF', parser, parser._parseHexColor.bind(parser));
});
test('Test class', function () {
let parser = new Parser();
assertNode('.faa', parser, parser._parseClass.bind(parser));
assertNode('faa', parser, parser._parseElementName.bind(parser));
assertNode('*', parser, parser._parseElementName.bind(parser));
assertNode('.faa42', parser, parser._parseClass.bind(parser));
});
test('Prio', function () {
let parser = new Parser();
assertNode('!important', parser, parser._parsePrio.bind(parser));
});
test('Expr', function () {
let parser = new Parser();
assertNode('45,5px', parser, parser._parseExpr.bind(parser));
assertNode(' 45 , 5px ', parser, parser._parseExpr.bind(parser));
assertNode('5/6', parser, parser._parseExpr.bind(parser));
assertNode('36mm, -webkit-calc(100%-10px)', parser, parser._parseExpr.bind(parser));
});
});
/*---------------------------------------------------------------------------------------------
* 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';
suite('CSS - Scanner', () => {
function assertSingleToken(scan: Scanner, source: string, len: number, offset: number, text: string, type: TokenType): void {
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);
}
test('Whitespace', function () {
let scanner = new Scanner();
assertSingleToken(scanner, ' @', 1, 1, '@', TokenType.Delim);
assertSingleToken(scanner, ' /* comment*/ \n/*comment*/@', 1, 26, '@', TokenType.Delim);
scanner = new Scanner();
scanner.ignoreWhitespace = false;
assertSingleToken(scanner, ' @', 1, 0, ' ', TokenType.Whitespace);
assertSingleToken(scanner, '/*comment*/ @', 1, 11, ' ', TokenType.Whitespace);
scanner = new Scanner();
scanner.ignoreComment = false;
assertSingleToken(scanner, ' /*comment*/@', 11, 1, '/*comment*/', TokenType.Comment);
assertSingleToken(scanner, '/*comment*/ @', 11, 0, '/*comment*/', TokenType.Comment);
});
test('Token Ident', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '\u060frf', 3, 0, '\u060frf', TokenType.Ident);
assertSingleToken(scanner, 'über', 4, 0, 'über', TokenType.Ident);
assertSingleToken(scanner, '-bo', 3, 0, '-bo', TokenType.Ident);
assertSingleToken(scanner, '_bo', 3, 0, '_bo', TokenType.Ident);
assertSingleToken(scanner, 'boo', 3, 0, 'boo', TokenType.Ident);
assertSingleToken(scanner, 'Boo', 3, 0, 'Boo', TokenType.Ident);
assertSingleToken(scanner, 'red--', 5, 0, 'red--', TokenType.Ident);
assertSingleToken(scanner, 'red-->', 5, 0, 'red--', TokenType.Ident);
assertSingleToken(scanner, '--red', 5, 0, '--red', TokenType.Ident);
assertSingleToken(scanner, 'a\\.b', 4, 0, 'a\.b', TokenType.Ident);
assertSingleToken(scanner, '\\E9motion', 9, 0, 'émotion', TokenType.Ident);
assertSingleToken(scanner, '\\E9 dition', 10, 0, 'édition', TokenType.Ident);
assertSingleToken(scanner, '\\0000E9dition', 13, 0, 'édition', TokenType.Ident);
assertSingleToken(scanner, 'S\\0000e9f', 9, 0, 'Séf', TokenType.Ident);
});
test('Token Url', function () {
let scanner = new Scanner();
assertSingleToken(scanner, 'url(\'http://msft.com\')', 22, 0, 'url(\'http://msft.com\')', TokenType.URI);
assertSingleToken(scanner, 'url("http://msft.com")', 22, 0, 'url("http://msft.com")', TokenType.URI);
assertSingleToken(scanner, 'url( "http://msft.com")', 23, 0, 'url( "http://msft.com")', TokenType.URI);
assertSingleToken(scanner, 'url(\t"http://msft.com")', 23, 0, 'url(\t"http://msft.com")', TokenType.URI);
assertSingleToken(scanner, 'url(\n"http://msft.com")', 23, 0, 'url(\n"http://msft.com")', TokenType.URI);
assertSingleToken(scanner, 'url("http://msft.com"\n)', 23, 0, 'url("http://msft.com"\n)', TokenType.URI);
assertSingleToken(scanner, 'url("")', 7, 0, 'url("")', TokenType.URI);
assertSingleToken(scanner, 'uRL("")', 7, 0, 'uRL("")', TokenType.URI);
assertSingleToken(scanner, 'URL("")', 7, 0, 'URL("")', TokenType.URI);
assertSingleToken(scanner, 'url(http://msft.com)', 20, 0, 'url(http://msft.com)', TokenType.URI);
assertSingleToken(scanner, 'url()', 5, 0, 'url()', TokenType.URI);
assertSingleToken(scanner, 'url(\'http://msft.com\n)', 22, 0, 'url(\'http://msft.com\n)', TokenType.BadUri);
assertSingleToken(scanner, 'url("http://msft.com"', 21, 0, 'url("http://msft.com"', TokenType.BadUri);
assertSingleToken(scanner, 'url(http://msft.com\')', 21, 0, 'url(http://msft.com\')', TokenType.URI);
});
test('Token AtKeyword', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '@import', 7, 0, '@import', TokenType.AtKeyword);
assertSingleToken(scanner, '@importttt', 10, 0, '@importttt', TokenType.AtKeyword);
assertSingleToken(scanner, '@imp', 4, 0, '@imp', TokenType.AtKeyword);
assertSingleToken(scanner, '@5', 2, 0, '@5', TokenType.AtKeyword);
assertSingleToken(scanner, '@media', 6, 0, '@media', TokenType.AtKeyword);
assertSingleToken(scanner, '@page', 5, 0, '@page', TokenType.AtKeyword);
assertSingleToken(scanner, '@charset', 8, 0, '@charset', TokenType.Charset);
assertSingleToken(scanner, '@-mport', 7, 0, '@-mport', TokenType.AtKeyword);
assertSingleToken(scanner, '@\u00f0mport', 7, 0, '@\u00f0mport', TokenType.AtKeyword);
assertSingleToken(scanner, '@', 1, 0, '@', TokenType.Delim);
});
test('Token Number', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '1234', 4, 0, '1234', TokenType.Num);
assertSingleToken(scanner, '1.34', 4, 0, '1.34', TokenType.Num);
assertSingleToken(scanner, '.234', 4, 0, '.234', TokenType.Num);
assertSingleToken(scanner, '.234.', 4, 0, '.234', TokenType.Num);
assertSingleToken(scanner, '..234', 1, 0, '.', TokenType.Delim);
});
test('Token Delim', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '@', 1, 0, '@', TokenType.Delim);
assertSingleToken(scanner, '+', 1, 0, '+', TokenType.Delim);
assertSingleToken(scanner, '>', 1, 0, '>', TokenType.Delim);
assertSingleToken(scanner, '#', 1, 0, '#', TokenType.Delim);
assertSingleToken(scanner, '\'', 1, 0, '\'', TokenType.BadString);
assertSingleToken(scanner, '"', 1, 0, '"', TokenType.BadString);
});
test('Token Hash', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '#import', 7, 0, '#import', TokenType.Hash);
assertSingleToken(scanner, '#-mport', 7, 0, '#-mport', TokenType.Hash);
assertSingleToken(scanner, '#123', 4, 0, '#123', TokenType.Hash);
});
test('Token Dimension/Percentage', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '3em', 3, 0, '3em', TokenType.EMS);
assertSingleToken(scanner, '4.423ex', 7, 0, '4.423ex', TokenType.EXS);
assertSingleToken(scanner, '3423px', 6, 0, '3423px', TokenType.Length);
assertSingleToken(scanner, '4.423cm', 7, 0, '4.423cm', TokenType.Length);
assertSingleToken(scanner, '4.423mm', 7, 0, '4.423mm', TokenType.Length);
assertSingleToken(scanner, '4.423in', 7, 0, '4.423in', TokenType.Length);
assertSingleToken(scanner, '4.423pt', 7, 0, '4.423pt', TokenType.Length);
assertSingleToken(scanner, '4.423pc', 7, 0, '4.423pc', TokenType.Length);
assertSingleToken(scanner, '4.423deg', 8, 0, '4.423deg', TokenType.Angle);
assertSingleToken(scanner, '4.423rad', 8, 0, '4.423rad', TokenType.Angle);
assertSingleToken(scanner, '4.423grad', 9, 0, '4.423grad', TokenType.Angle);
assertSingleToken(scanner, '4.423ms', 7, 0, '4.423ms', TokenType.Time);
assertSingleToken(scanner, '4.423s', 6, 0, '4.423s', TokenType.Time);
assertSingleToken(scanner, '4.423hz', 7, 0, '4.423hz', TokenType.Freq);
assertSingleToken(scanner, '.423khz', 7, 0, '.423khz', TokenType.Freq);
assertSingleToken(scanner, '3.423%', 6, 0, '3.423%', TokenType.Percentage);
assertSingleToken(scanner, '.423%', 5, 0, '.423%', TokenType.Percentage);
assertSingleToken(scanner, '.423ft', 6, 0, '.423ft', TokenType.Dimension);
assertSingleToken(scanner, '200dpi', 6, 0, '200dpi', TokenType.Resolution);
assertSingleToken(scanner, '123dpcm', 7, 0, '123dpcm', TokenType.Resolution);
});
test('Token String', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '\'farboo\'', 8, 0, '\'farboo\'', TokenType.String);
assertSingleToken(scanner, '"farboo"', 8, 0, '"farboo"', TokenType.String);
assertSingleToken(scanner, '"farbo\u00f0"', 8, 0, '"farbo\u00f0"', TokenType.String);
assertSingleToken(scanner, '"far\\\"oo"', 9, 0, '"far\"oo"', TokenType.String);
assertSingleToken(scanner, '"fa\\\noo"', 8, 0, '"fa\noo"', TokenType.String);
assertSingleToken(scanner, '"fa\\\roo"', 8, 0, '"fa\roo"', TokenType.String);
assertSingleToken(scanner, '"fa\\\foo"', 8, 0, '"fa\foo"', TokenType.String);
assertSingleToken(scanner, '\'farboo"', 8, 0, '\'farboo"', TokenType.BadString);
assertSingleToken(scanner, '\'farboo', 7, 0, '\'farboo', TokenType.BadString);
assertSingleToken(scanner, '\'', 1, 0, '\'', TokenType.BadString);
assertSingleToken(scanner, '"', 1, 0, '"', TokenType.BadString);
});
test('Token CDO', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '<!--', 4, 0, '<!--', TokenType.CDO);
assertSingleToken(scanner, '<!-\n-', 1, 0, '<', TokenType.Delim);
});
test('Token CDC', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '-->', 3, 0, '-->', TokenType.CDC);
assertSingleToken(scanner, '--y>', 3, 0, '--y', TokenType.Ident);
assertSingleToken(scanner, '--<', 1, 0, '-', TokenType.Delim);
});
test('Token singletokens ;:{}[]()', function () {
let scanner = new Scanner();
assertSingleToken(scanner, ': ', 1, 0, ':', TokenType.Colon);
assertSingleToken(scanner, '; ', 1, 0, ';', TokenType.SemiColon);
assertSingleToken(scanner, '{ ', 1, 0, '{', TokenType.CurlyL);
assertSingleToken(scanner, '} ', 1, 0, '}', TokenType.CurlyR);
assertSingleToken(scanner, '[ ', 1, 0, '[', TokenType.BracketL);
assertSingleToken(scanner, '] ', 1, 0, ']', TokenType.BracketR);
assertSingleToken(scanner, '( ', 1, 0, '(', TokenType.ParenthesisL);
assertSingleToken(scanner, ') ', 1, 0, ')', TokenType.ParenthesisR);
});
test('Token dashmatch & includes', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '~=', 2, 0, '~=', TokenType.Includes);
assertSingleToken(scanner, '~', 1, 0, '~', TokenType.Delim);
assertSingleToken(scanner, '|=', 2, 0, '|=', TokenType.Dashmatch);
assertSingleToken(scanner, '|', 1, 0, '|', TokenType.Delim);
assertSingleToken(scanner, '^=', 2, 0, '^=', TokenType.PrefixOperator);
assertSingleToken(scanner, '$=', 2, 0, '$=', TokenType.SuffixOperator);
assertSingleToken(scanner, '*=', 2, 0, '*=', TokenType.SubstringOperator);
});
test('Comments', function () {
let scanner = new Scanner();
assertSingleToken(scanner, '/* */', 0, 10, '', TokenType.EOF);
assertSingleToken(scanner, '/* abcd*/', 0, 14, '', TokenType.EOF);
assertSingleToken(scanner, '/*abcd */', 0, 10, '', TokenType.EOF);
assertSingleToken(scanner, '/* ab- .-cd */', 0, 15, '', TokenType.EOF);
});
test('Whitespaces', function () {
let scanner = new Scanner();
assertSingleToken(scanner, ' ', 0, 1, '', TokenType.EOF);
assertSingleToken(scanner, ' ', 0, 6, '', TokenType.EOF);
});
});
suite('CSS - Token Sequences', () => {
function assertTokenSequence(scan: Scanner, source: string, ...tokens: TokenType[]): void {
scan.setSource(source);
let token = scan.scan();
let i = 0;
while (tokens.length > i) {
assert.equal(token.type, tokens[i]);
token = scan.scan();
i++;
}
}
// tests with skipping comments
test('Token Sequence', function () {
let scanner = new Scanner();
assertTokenSequence(scanner, '5 5 5 5', TokenType.Num, TokenType.Num, TokenType.Num, TokenType.Num);
assertTokenSequence(scanner, '/* 5 4 */-->', TokenType.CDC);
assertTokenSequence(scanner, '/* 5 4 */ -->', TokenType.CDC);
assertTokenSequence(scanner, '/* "adaasd" */ -->', TokenType.CDC);
assertTokenSequence(scanner, '/* <!-- */ -->', TokenType.CDC);
assertTokenSequence(scanner, 'red-->', TokenType.Ident, TokenType.Delim);
assertTokenSequence(scanner, '@ import', TokenType.Delim, TokenType.Ident);
});
});
/*---------------------------------------------------------------------------------------------
* 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 * as nodes from '../parser/cssNodes';
import * as selectorPrinter from '../services/selectorPrinting';
import {TextDocument} from 'vscode-languageserver';
function elementToString(element: selectorPrinter.Element): string {
let label = element.name || '';
if (element.attributes) {
label = label + '[';
let needsSeparator = false;
for (let key in element.attributes) {
if (needsSeparator) {
label = label + '|';
}
needsSeparator = true;
label = label + key + '=' + element.attributes[key];
}
label = label + ']';
}
if (element.children) {
label = label + '{';
for (let index = 0; index < element.children.length; index++) {
if (index > 0) {
label = label + '|';
}
label = label + elementToString(element.children[index]);
}
label = label + '}';
}
return label;
}
export function parseSelector(p: Parser, input: string, selectorName: string, expected: string): void {
let document = TextDocument.create('test://test/test.css', 'css', 0, input);
let styleSheet = p.parseStylesheet(document);
let node = nodes.getNodeAtOffset(styleSheet, input.indexOf(selectorName));
let selector = node.findParent(nodes.NodeType.Selector);
let element = selectorPrinter.selectorToElement(selector);
assert.equal(elementToString(element), expected);
}
export interface ExpectedElement {
name?: string;
attributes?: { [name: string]: string; };
}
export function assertElement(p: Parser, input: string, element: ExpectedElement): void {
let node = p.internalParse(input, p._parseSimpleSelector);
let actual = selectorPrinter.toElement(node);
assert.equal(actual.name, element.name);
assert.deepEqual(actual.attributes, element.attributes);
}
suite('CSS - Selector Printing', () => {
test('class/hash/elementname/attr', function () {
let p = new Parser();
assertElement(p, 'element', { name: 'element' });
assertElement(p, '.div', { attributes: { class: 'div' } });
assertElement(p, '#first', { attributes: { id: 'first' } });
assertElement(p, 'element.on', { name: 'element', attributes: { class: 'on' } });
assertElement(p, 'element.on#first', { name: 'element', attributes: { class: 'on', id: 'first' } });
assertElement(p, '.on#first', { attributes: { class: 'on', id: 'first' } });
assertElement(p, '[lang=\'de\']', { attributes: { lang: 'de' } });
assertElement(p, '[enabled]', { attributes: { enabled: undefined } });
});
test('simple selector', function () {
let p = new Parser();
parseSelector(p, 'element { }', 'element', '{element}');
parseSelector(p, 'element.div { }', 'element', '{element[class=div]}');
parseSelector(p, 'element.on#first { }', 'element', '{element[class=on|id=first]}');
parseSelector(p, 'element:hover { }', 'element', '{element[:hover=]}');
parseSelector(p, 'element[lang=\'de\'] { }', 'element', '{element[lang=de]}');
parseSelector(p, 'element[enabled] { }', 'element', '{element[enabled=undefined]}');
parseSelector(p, 'element[foo~="warning"] { }', 'element', '{element[foo= … warning … ]}');
parseSelector(p, 'element[lang|="en"] { }', 'element', '{element[lang=en-…]}');
parseSelector(p, '* { }', '*', '{element}');
});
test('selector', function () {
let p = new Parser();
parseSelector(p, 'e1 e2 { }', 'e1', '{e1{…{e2}}}');
parseSelector(p, 'e1 .div { }', 'e1', '{e1{…{[class=div]}}}');
parseSelector(p, 'e1 > e2 { }', 'e2', '{e1{e2}}');
parseSelector(p, 'e1, e2 { }', 'e1', '{e1}');
parseSelector(p, 'e1 + e2 { }', 'e2', '{e1|e2}');
parseSelector(p, 'e1 ~ e2 { }', 'e2', '{e1|e2|⋮|e2}');
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 {TextDocument, TextEdit} from 'vscode-languageserver';
import * as assert from 'assert';
export function applyEdits(document: TextDocument, edits: TextEdit[]): string {
let text = document.getText();
let sortedEdits = edits.sort((a, b) => document.offsetAt(b.range.start) - document.offsetAt(a.range.start));
let lastOffset = text.length;
sortedEdits.forEach(e => {
let startOffset = document.offsetAt(e.range.start);
let endOffset = document.offsetAt(e.range.end);
assert.ok(startOffset <= endOffset);
assert.ok(endOffset <= lastOffset);
text = text.substring(0, startOffset) + e.newText + text.substring(endOffset, text.length);
lastOffset = startOffset;
});
return text;
}
\ No newline at end of file
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/**
* The Thenable (E.g. PromiseLike) and Promise declarions are taken from TypeScript's
* lib.core.es6.d.ts file. See above Copyright notice.
*/
/**
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
* and others. This API makes no assumption about what promise libary is being used which
* enables reusing existing code without migrating to a specific promise implementation. Still,
* we recommand the use of native promises which are available in VS Code.
*/
interface Thenable<R> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: R) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: R) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
}
/**
* Represents the completion of an asynchronous operation
*/
interface Promise<T> extends Thenable<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Promise<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Promise<TResult>;
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch(onrejected?: (reason: any) => T | Thenable<T>): Promise<T>;
}
interface PromiseConstructor {
/**
* Creates a new Promise.
* @param executor A callback used to initialize the promise. This callback is passed two arguments:
* a resolve callback used resolve the promise with a value or the result of another promise,
* and a reject callback used to reject the promise with a provided reason or error.
*/
new <T>(executor: (resolve: (value?: T | Thenable<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
/**
* Creates a Promise that is resolved with an array of results when all of the provided Promises
* resolve, or rejected when any Promise is rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
all<T>(values: Array<T | Thenable<T>>): Promise<T[]>;
/**
* Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
* or rejected.
* @param values An array of Promises.
* @returns A new Promise.
*/
race<T>(values: Array<T | Thenable<T>>): Promise<T>;
/**
* Creates a new rejected promise for the provided reason.
* @param reason The reason the promise was rejected.
* @returns A new rejected Promise.
*/
reject(reason: any): Promise<void>;
/**
* Creates a new rejected promise for the provided reason.
* @param reason The reason the promise was rejected.
* @returns A new rejected Promise.
*/
reject<T>(reason: any): Promise<T>;
/**
* Creates a new resolved promise for the provided value.
* @param value A promise.
* @returns A promise whose internal state matches the provided promise.
*/
resolve<T>(value: T | Thenable<T>): Promise<T>;
/**
* Creates a new resolved promise .
* @returns A resolved promise.
*/
resolve(): Promise<void>;
}
declare var Promise: PromiseConstructor;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../../src/typings/mocha.d.ts'/>
/// <reference path='../../../../../extensions/node.d.ts'/>
/// <reference path='../../../../../extensions/lib.core.d.ts'/>
/// <reference path='../../../../../extensions/declares.d.ts'/>
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
/**
* Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false
* are located before all elements where p(x) is true.
* @returns the least x for which p(x) is true or array.length if no element fullfills the given function.
*/
export function findFirst<T>(array: T[], p: (x: T) => boolean): number {
let low = 0, high = array.length;
if (high === 0) {
return 0; // no children
}
while (low < high) {
let mid = Math.floor((low + high) / 2);
if (p(array[mid])) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export function startsWith(haystack: string, needle: string): boolean {
if (haystack.length < needle.length) {
return false;
}
for (let i = 0; i < needle.length; i++) {
if (haystack[i] !== needle[i]) {
return false;
}
}
return true;
}
/**
* Determines if haystack ends with needle.
*/
export function endsWith(haystack: string, needle: string): boolean {
let diff = haystack.length - needle.length;
if (diff > 0) {
return haystack.lastIndexOf(needle) === diff;
} else if (diff === 0) {
return haystack === needle;
} else {
return false;
}
}
/**
* Computes the difference score for two strings. More similar strings have a higher score.
* We use largest common subsequence dynamic programming approach but penalize in the end for length differences.
* Strings that have a large length difference will get a bad default score 0.
* Complexity - both time and space O(first.length * second.length)
* Dynamic programming LCS computation http://en.wikipedia.org/wiki/Longest_common_subsequence_problem
*
* @param first a string
* @param second a string
*/
export function difference(first: string, second: string, maxLenDelta: number = 4): number {
let lengthDifference = Math.abs(first.length - second.length);
// We only compute score if length of the currentWord and length of entry.name are similar.
if (lengthDifference > maxLenDelta) {
return 0;
}
// Initialize LCS (largest common subsequence) matrix.
let LCS: number[][] = [];
let zeroArray: number[] = [];
let i: number, j: number;
for (i = 0; i < second.length + 1; ++i) {
zeroArray.push(0);
}
for (i = 0; i < first.length + 1; ++i) {
LCS.push(zeroArray);
}
for (i = 1; i < first.length + 1; ++i) {
for (j = 1; j < second.length + 1; ++j) {
if (first[i - 1] === second[j - 1]) {
LCS[i][j] = LCS[i - 1][j - 1] + 1;
} else {
LCS[i][j] = Math.max(LCS[i - 1][j], LCS[i][j - 1]);
}
}
}
return LCS[first.length][second.length] - Math.sqrt(lengthDifference);
}
--ui tdd
--useColors true
./out/test
{
"compilerOptions": {
"noLib": true,
"target": "es5",
"module": "commonjs",
"sourceMap": true,
"sourceRoot": "../src",
"outDir": "./out"
},
"exclude": [
"node_modules"
]
}
\ No newline at end of file
......@@ -5,10 +5,11 @@
'use strict';
import 'vs/languages/css/common/css.contribution';
import 'vs/languages/handlebars/common/handlebars.contribution';
import 'vs/languages/html/common/html.contribution';
import 'vs/languages/markdown/common/markdown.contribution';
import 'vs/languages/razor/common/razor.contribution';
import 'vs/languages/less/common/less.contribution';
import 'vs/languages/sass/common/sass.contribution';
//import 'vs/languages/less/common/less.contribution';
//import 'vs/languages/sass/common/sass.contribution';
//import 'vs/languages/css/common/css.contribution';
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册