From 4dbc30c5e7a2e224420f45c237bf690713e8b4d2 Mon Sep 17 00:00:00 2001 From: Roman Dvornov Date: Tue, 14 Dec 2021 18:39:25 +0100 Subject: [PATCH] Add sass/less custom at-rules --- README.md | 2 +- lib/syntax-extension/index.js | 139 +---------------------------- lib/syntax-extension/less/index.js | 88 ++++++++++++++++++ lib/syntax-extension/sass/index.js | 120 +++++++++++++++++++++++++ test/preprocessors.js | 39 +++++++- 5 files changed, 247 insertions(+), 141 deletions(-) create mode 100644 lib/syntax-extension/less/index.js create mode 100644 lib/syntax-extension/sass/index.js diff --git a/README.md b/README.md index 6b03342..f287a02 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Since the plugin focuses on CSS syntax validation it warns on a syntax which is By default the plugin exams styles as pure CSS. To specify that a preprocessor's syntax is used, you must specify an array with the names of these extensions. Currently supported: -- `sass` – declaration values with Sass syntax will be ignored as well as custom at-rules introduced by Saas (e.g. `@if`, `@else`, `@mixin` etc) +- `sass` – declaration values with Sass syntax will be ignored as well as custom at-rules introduced by Saas (e.g. `@if`, `@else`, `@mixin` etc). For now Sass at-rules are allowed with any prelude, but it might be replaced for real syntax definitions in future releases - `less` – declaration values with Sass syntax will be ignored as well as `@plugin` at-rule introduced by Less Using both syntax extensions is also possible: diff --git a/lib/syntax-extension/index.js b/lib/syntax-extension/index.js index a62fd09..f7cbf46 100644 --- a/lib/syntax-extension/index.js +++ b/lib/syntax-extension/index.js @@ -1,137 +1,2 @@ -import { tokenTypes as TYPE } from 'css-tree'; -import * as LessVariableReference from './less/LessVariableReference.js'; -import * as LessVariable from './less/LessVariable.js'; -import * as LessEscaping from './less/LessEscaping.js'; -import * as LessNamespace from './less/LessNamespace.js'; -import * as SassVariable from './sass/SassVariable.js'; -import * as SassInterpolation from './sass/SassInterpolation.js'; -import * as SassNamespace from './sass/SassNamespace.js'; - -const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) -const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) -const PERCENTAGESIGN = 0x0025; // U+0025 PERCENTAGE SIGN (%) -const FULLSTOP = 0x002E; // U+002E FULL STOP (.) -const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) -const COMMERCIALAT = 0x0040; // U+0040 COMMERCIAL AT (@) -const TILDE = 0x007E; // U+007E TILDE (~) - -// custom error -class PreprocessorExtensionError { - constructor() { - this.type = 'PreprocessorExtensionError'; - } -} - -export function less(syntaxConfig) { - // new node types - syntaxConfig.node.LessVariableReference = LessVariableReference; - syntaxConfig.node.LessVariable = LessVariable; - syntaxConfig.node.LessEscaping = LessEscaping; - syntaxConfig.node.LessNamespace = LessNamespace; - - // extend parser value parser - const originalGetNode = syntaxConfig.scope.Value.getNode; - syntaxConfig.scope.Value.getNode = function(context) { - let node = null; - - switch (this.tokenType) { - case TYPE.AtKeyword: // less: @var - node = this.LessVariable(); - break; - - case TYPE.Hash: { - let sc = 0; - let tokenType = 0; - - // deprecated - do { - tokenType = this.lookupType(++sc); - if (tokenType !== TYPE.WhiteSpace && tokenType !== TYPE.Comment) { - break; - } - } while (tokenType !== TYPE.EOF); - - if (this.isDelim(FULLSTOP, sc) || /* preferred */ - this.isDelim(GREATERTHANSIGN, sc) /* deprecated */) { - node = this.LessNamespace(); - } - - break; - } - - case TYPE.Delim: - switch (this.source.charCodeAt(this.tokenStart)) { - case COMMERCIALAT: // less: @@var - if (this.lookupType(1) === TYPE.AtKeyword) { - node = this.LessVariableReference(); - } - break; - - case TILDE: // less: ~"asd" | ~'asd' - node = this.LessEscaping(); - break; - - - } - - break; - } - - // currently we can't validate values that contain less/sass extensions - if (node !== null) { - throw new PreprocessorExtensionError(); - } - - return originalGetNode.call(this, context); - }; - - return syntaxConfig; -} - -export function sass(syntaxConfig) { - // new node types - syntaxConfig.node.SassVariable = SassVariable; - syntaxConfig.node.SassInterpolation = SassInterpolation; - syntaxConfig.node.SassNamespace = SassNamespace; - - // extend parser value parser - const originalGetNode = syntaxConfig.scope.Value.getNode; - syntaxConfig.scope.Value.getNode = function(context) { - let node = null; - - switch (this.tokenType) { - case TYPE.Ident: - if (this.isDelim(FULLSTOP, 1)) { - node = this.SassNamespace(); - } - break; - - case TYPE.Delim: - switch (this.source.charCodeAt(this.tokenStart)) { - case DOLLARSIGN: // sass: $var - node = this.SassVariable(); - break; - - case NUMBERSIGN: // sass: #{ } - if (this.lookupType(1) === TYPE.LeftCurlyBracket) { - node = this.SassInterpolation(this.scope.Value, this.readSequence); - } - break; - - case PERCENTAGESIGN: // sass: 5 % 4 - node = this.Operator(); - break; - } - break; - } - - // currently we can't validate values that contain less/sass extensions - if (node !== null) { - throw new PreprocessorExtensionError(); - } - - return originalGetNode.call(this, context); - }; - - return syntaxConfig; -}; +export { default as less } from './less/index.js'; +export { default as sass } from './sass/index.js'; diff --git a/lib/syntax-extension/less/index.js b/lib/syntax-extension/less/index.js new file mode 100644 index 0000000..a9149f1 --- /dev/null +++ b/lib/syntax-extension/less/index.js @@ -0,0 +1,88 @@ +import { tokenTypes as TYPE } from 'css-tree'; +import * as LessVariableReference from './LessVariableReference.js'; +import * as LessVariable from './LessVariable.js'; +import * as LessEscaping from './LessEscaping.js'; +import * as LessNamespace from './LessNamespace.js'; + +const FULLSTOP = 0x002E; // U+002E FULL STOP (.) +const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) +const COMMERCIALAT = 0x0040; // U+0040 COMMERCIAL AT (@) +const TILDE = 0x007E; // U+007E TILDE (~) + +// custom error +class PreprocessorExtensionError { + constructor() { + this.type = 'PreprocessorExtensionError'; + } +} + +export default function less(syntaxConfig) { + // new node types + syntaxConfig.node.LessVariableReference = LessVariableReference; + syntaxConfig.node.LessVariable = LessVariable; + syntaxConfig.node.LessEscaping = LessEscaping; + syntaxConfig.node.LessNamespace = LessNamespace; + + // custom at-rules + syntaxConfig.atrules.plugin = { + prelude: '' + }; + + // extend parser value parser + const originalGetNode = syntaxConfig.scope.Value.getNode; + syntaxConfig.scope.Value.getNode = function(context) { + let node = null; + + switch (this.tokenType) { + case TYPE.AtKeyword: // less: @var + node = this.LessVariable(); + break; + + case TYPE.Hash: { + let sc = 0; + let tokenType = 0; + + // deprecated + do { + tokenType = this.lookupType(++sc); + if (tokenType !== TYPE.WhiteSpace && tokenType !== TYPE.Comment) { + break; + } + } while (tokenType !== TYPE.EOF); + + if (this.isDelim(FULLSTOP, sc) || /* preferred */ + this.isDelim(GREATERTHANSIGN, sc) /* deprecated */) { + node = this.LessNamespace(); + } + + break; + } + + case TYPE.Delim: + switch (this.source.charCodeAt(this.tokenStart)) { + case COMMERCIALAT: // less: @@var + if (this.lookupType(1) === TYPE.AtKeyword) { + node = this.LessVariableReference(); + } + break; + + case TILDE: // less: ~"asd" | ~'asd' + node = this.LessEscaping(); + break; + + + } + + break; + } + + // currently we can't validate values that contain less/sass extensions + if (node !== null) { + throw new PreprocessorExtensionError(); + } + + return originalGetNode.call(this, context); + }; + + return syntaxConfig; +} diff --git a/lib/syntax-extension/sass/index.js b/lib/syntax-extension/sass/index.js new file mode 100644 index 0000000..7cc5370 --- /dev/null +++ b/lib/syntax-extension/sass/index.js @@ -0,0 +1,120 @@ +import { tokenTypes as TYPE } from 'css-tree'; +import * as SassVariable from './SassVariable.js'; +import * as SassInterpolation from './SassInterpolation.js'; +import * as SassNamespace from './SassNamespace.js'; + +const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) +const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) +const PERCENTAGESIGN = 0x0025; // U+0025 PERCENTAGE SIGN (%) +const FULLSTOP = 0x002E; // U+002E FULL STOP (.) + +// custom error +class PreprocessorExtensionError { + constructor() { + this.type = 'PreprocessorExtensionError'; + } +} + +export default function sass(syntaxConfig) { + // new node types + syntaxConfig.node.SassVariable = SassVariable; + syntaxConfig.node.SassInterpolation = SassInterpolation; + syntaxConfig.node.SassNamespace = SassNamespace; + + // custom at-rules + syntaxConfig.atrules['at-root'] = { + prelude: '' + }; + syntaxConfig.atrules.content = { + prelude: '' + }; + syntaxConfig.atrules.debug = { + prelude: '' + }; + syntaxConfig.atrules.each = { + prelude: '' + }; + syntaxConfig.atrules.else = { + prelude: '' + }; + syntaxConfig.atrules.error = { + prelude: '' + }; + syntaxConfig.atrules.extend = { + prelude: '' + }; + syntaxConfig.atrules.for = { + prelude: '' + }; + syntaxConfig.atrules.forward = { + prelude: '' + }; + syntaxConfig.atrules.function = { + prelude: '' + }; + syntaxConfig.atrules.if = { + prelude: '' + }; + syntaxConfig.atrules.import = { + prelude: syntaxConfig.atrules.import.prelude + '| #' // FIXME: fix prelude extension in css-tree + }; + syntaxConfig.atrules.include = { + prelude: '' + }; + syntaxConfig.atrules.mixin = { + prelude: '' + }; + syntaxConfig.atrules.return = { + prelude: '' + }; + syntaxConfig.atrules.use = { + prelude: '' + }; + syntaxConfig.atrules.warn = { + prelude: '' + }; + syntaxConfig.atrules.while = { + prelude: '' + }; + + // extend parser value parser + const originalGetNode = syntaxConfig.scope.Value.getNode; + syntaxConfig.scope.Value.getNode = function(context) { + let node = null; + + switch (this.tokenType) { + case TYPE.Ident: + if (this.isDelim(FULLSTOP, 1)) { + node = this.SassNamespace(); + } + break; + + case TYPE.Delim: + switch (this.source.charCodeAt(this.tokenStart)) { + case DOLLARSIGN: // sass: $var + node = this.SassVariable(); + break; + + case NUMBERSIGN: // sass: #{ } + if (this.lookupType(1) === TYPE.LeftCurlyBracket) { + node = this.SassInterpolation(this.scope.Value, this.readSequence); + } + break; + + case PERCENTAGESIGN: // sass: 5 % 4 + node = this.Operator(); + break; + } + break; + } + + // currently we can't validate values that contain less/sass extensions + if (node !== null) { + throw new PreprocessorExtensionError(); + } + + return originalGetNode.call(this, context); + }; + + return syntaxConfig; +}; diff --git a/test/preprocessors.js b/test/preprocessors.js index a4a4f93..8f12bac 100644 --- a/test/preprocessors.js +++ b/test/preprocessors.js @@ -43,7 +43,7 @@ const lessTests = function(tr) { tr.ok('.foo { color: #outer.inner(1 + 2); }'); // preferred // custom at-rules - // tr.ok('@plugin "my-plugin";'); + tr.ok('@plugin "my-plugin";'); }; const sassTests = function(tr) { @@ -71,12 +71,45 @@ const sassTests = function(tr) { tr.ok('.foo { color: fonts.$font-family-text; }'); tr.ok('.foo { color: fonts.foo(); }'); tr.ok('.foo { color: fonts.foo(1 + 2); }'); + + // custom at-rules + tr.ok('@at-root xxx'); + tr.ok('@content xxx'); + tr.ok('@debug xxx'); + tr.ok('@each xxx'); + tr.ok('@else xxx'); + tr.ok('@error xxx'); + tr.ok('@extend xxx'); + tr.ok('@for xxx'); + tr.ok('@forward xxx'); + tr.ok('@function xxx'); + tr.ok('@if xxx'); + tr.ok('@import "theme.css";'); + tr.ok('@import "http://fonts.googleapis.com/css?family=Droid+Sans";'); + tr.ok('@import url(theme);'); + tr.ok('@import "landscape" screen and (orientation: landscape);'); + tr.ok('@include xxx'); + tr.ok('@mixin xxx'); + tr.ok('@return xxx'); + tr.ok('@use xxx'); + tr.ok('@warn xxx'); + tr.ok('@while xxx'); }; // less extenstions -less({ syntaxExtensions: ['less'] }, lessTests); +less({ syntaxExtensions: ['less'] }, (tr) => { + lessTests(tr); + + // custom Sass at-rules + tr.notOk('@if $test {}', messages.unknownAtrule('if')); +}); less({ syntaxExtensions: ['less', 'sass'] }, lessTests); // sass extenstions -sass({ syntaxExtensions: ['sass'] }, sassTests); +sass({ syntaxExtensions: ['sass'] }, (tr) => { + sassTests(tr); + + // custom Sass at-rules + tr.notOk('@plugin "foo";', messages.unknownAtrule('plugin')); +}); sass({ syntaxExtensions: ['sass', 'less'] }, sassTests);