From a20f26ba07197422db14f6d4e2e8274aff8262d7 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 23 Jun 2018 09:05:24 -0700 Subject: [PATCH 1/4] WIP - Fixes Mixin call args not being visited --- .eslintrc.json | 5 +-- .vscode/launch.json | 19 +++++++++ .vscode/tasks.json | 9 ++++ Gruntfile.js | 2 +- lib/less/parser/parser-input.js | 1 - lib/less/parser/parser.js | 66 +++++++++++++++++++----------- lib/less/transform-tree.js | 36 ++++++++++++---- lib/less/visitors/visitor.js | 22 +++++----- test/css/plugin-preeval.css | 3 ++ test/less/plugin-preeval.less | 15 +++++++ test/less/plugin/plugin-preeval.js | 50 ++++++++++++++++++++++ 11 files changed, 180 insertions(+), 48 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 test/css/plugin-preeval.css create mode 100644 test/less/plugin-preeval.less create mode 100644 test/less/plugin/plugin-preeval.js diff --git a/.eslintrc.json b/.eslintrc.json index 754c3845e..6a9e809eb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,11 +3,8 @@ "node": true }, "globals": {}, + "rules": { - "quotes": [ - 1, - "single" - ], "no-eval": 2, "no-use-before-define": [ 2, diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..e36b66050 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Less Test (Grunt)", + "program": "${workspaceFolder}/node_modules/grunt/lib/grunt/cli.js", + "cwd": "${workspaceFolder}", + "console":"integratedTerminal", + "args": [ + "test" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..60702afa9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,9 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "0.1.0", + "command": "grunt", + "isShellCommand": true, + "args": ["test"], + "showOutput": "always" +} \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index c8bbde7a1..1e68e68bb 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -295,7 +295,7 @@ module.exports = function (grunt) { main: { // src is used to build list of less files to compile src: [ - 'test/less/*.less', + 'test/less/plugin.less', // Don't test NPM import, obviously '!test/less/plugin-module.less', '!test/less/import-module.less', diff --git a/lib/less/parser/parser-input.js b/lib/less/parser/parser-input.js index bf99ef789..b240f1d4f 100644 --- a/lib/less/parser/parser-input.js +++ b/lib/less/parser/parser-input.js @@ -239,7 +239,6 @@ module.exports = function() { case "/": if (input.charAt(i + 1) === "*") { i++; - console.log(input.substr(lastPos, i - lastPos)); inComment = true; blockDepth++; } diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index 2b31eb7d4..b935461ed 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -1342,36 +1342,54 @@ var Parser = function Parser(context, imports, fileInfo) { * until it reaches outer-most tokens. */ permissiveValue: function (untilTokens) { - var i, index = parserInput.i, + var i, e, index = parserInput.i, value = []; + + do { + e = this.comment(); + if (e) { + value.push(e); + continue; + } + e = this.entity(); + if (e) { + value.push(e); + } + } while (e); + + if (value.length > 0) { + return new(tree.Expression)(value); + } + else { value = parserInput.$parseUntil(untilTokens); - if (value) { - if (typeof value === 'string') { - error("Expected '" + value + "'", "Parse"); - } - if (value.length === 1 && value[0] === ' ') { - return new tree.Anonymous('', index); - } - var item, args = []; - for (i = 0; i < value.length; i++) { - item = value[i]; - if (Array.isArray(item)) { - // Treat actual quotes as normal quoted values - args.push(new tree.Quoted(item[0], item[1], true, index, fileInfo)); + if (value) { + if (typeof value === 'string') { + error("Expected '" + value + "'", "Parse"); } - else { - if (i === value.length - 1) { - item = item.trim(); + if (value.length === 1 && value[0] === ' ') { + return new tree.Anonymous('', index); + } + var item, args = []; + for (i = 0; i < value.length; i++) { + item = value[i]; + if (Array.isArray(item)) { + // Treat actual quotes as normal quoted values + args.push(new tree.Quoted(item[0], item[1], true, index, fileInfo)); + } + else { + if (i === value.length - 1) { + item = item.trim(); + } + // Treat like quoted values, but replace vars like unquoted expressions + var quote = new tree.Quoted("'", item, true, index, fileInfo); + quote.variableRegex = /@([\w-]+)/g; + quote.propRegex = /\$([\w-]+)/g; + quote.reparse = true; + args.push(quote); } - // Treat like quoted values, but replace vars like unquoted expressions - var quote = new tree.Quoted("'", item, true, index, fileInfo); - quote.variableRegex = /@([\w-]+)/g; - quote.propRegex = /\$([\w-]+)/g; - quote.reparse = true; - args.push(quote); } + return new tree.Expression(args, true); } - return new tree.Expression(args, true); } }, diff --git a/lib/less/transform-tree.js b/lib/less/transform-tree.js index d690d6930..56173023e 100644 --- a/lib/less/transform-tree.js +++ b/lib/less/transform-tree.js @@ -41,29 +41,49 @@ module.exports = function(root, options) { new visitor.MarkVisibleSelectorsVisitor(true), new visitor.ExtendVisitor(), new visitor.ToCSSVisitor({compress: Boolean(options.compress)}) - ], v, visitorIterator; + ], preEvalVisitors = [], v, visitorIterator; - // first() / get() allows visitors to be added while visiting + /** + * first() / get() allows visitors to be added while visiting + * + * @todo Add scoping for visitors just like functions for @plugin; right now they're global + */ if (options.pluginManager) { visitorIterator = options.pluginManager.visitor(); - visitorIterator.first(); - while ((v = visitorIterator.get())) { - if (v.isPreEvalVisitor) { - v.run(root); + for (var i = 0; i < 2; i++) { + visitorIterator.first(); + while ((v = visitorIterator.get())) { + if (v.isPreEvalVisitor) { + if (i === 0 || preEvalVisitors.indexOf(v) === -1) { + preEvalVisitors.push(v); + v.run(root); + } + } + else { + if (i === 0 || visitors.indexOf(v) === -1) { + if (v.isPreVisitor) { + visitors.unshift(v); + } + else { + visitors.push(v); + } + } + } } } } - + evaldRoot = root.eval(evalEnv); for (var i = 0; i < visitors.length; i++) { visitors[i].run(evaldRoot); } + // Run any remaining visitors added after eval pass if (options.pluginManager) { visitorIterator.first(); while ((v = visitorIterator.get())) { - if (!v.isPreEvalVisitor) { + if (visitors.indexOf(v) === -1 && preEvalVisitors.indexOf(v) === -1) { v.run(evaldRoot); } } diff --git a/lib/less/visitors/visitor.js b/lib/less/visitors/visitor.js index cf67040c0..7b947e165 100644 --- a/lib/less/visitors/visitor.js +++ b/lib/less/visitors/visitor.js @@ -32,7 +32,8 @@ function indexNodeTypes(parent, ticker) { var Visitor = function(implementation) { this._implementation = implementation; - this._visitFnCache = []; + this._visitInCache = {}; + this._visitOutCache = {}; if (!_hasIndexed) { indexNodeTypes(tree, 1); @@ -48,15 +49,16 @@ Visitor.prototype = { var nodeTypeIndex = node.typeIndex; if (!nodeTypeIndex) { + // MixinCall args aren't a node type? + if (node.value && node.value.typeIndex) { + this.visit(node.value); + } return node; } - var visitFnCache = this._visitFnCache, - impl = this._implementation, - aryIndx = nodeTypeIndex << 1, - outAryIndex = aryIndx | 1, - func = visitFnCache[aryIndx], - funcOut = visitFnCache[outAryIndex], + var impl = this._implementation, + func = this._visitInCache[nodeTypeIndex], + funcOut = this._visitOutCache[nodeTypeIndex], visitArgs = _visitArgs, fnName; @@ -66,13 +68,13 @@ Visitor.prototype = { fnName = "visit" + node.type; func = impl[fnName] || _noop; funcOut = impl[fnName + "Out"] || _noop; - visitFnCache[aryIndx] = func; - visitFnCache[outAryIndex] = funcOut; + this._visitInCache[nodeTypeIndex] = func; + this._visitOutCache[nodeTypeIndex] = funcOut; } if (func !== _noop) { var newNode = func.call(impl, node, visitArgs); - if (impl.isReplacing) { + if (node && impl.isReplacing) { node = newNode; } } diff --git a/test/css/plugin-preeval.css b/test/css/plugin-preeval.css new file mode 100644 index 000000000..f43d8a9cd --- /dev/null +++ b/test/css/plugin-preeval.css @@ -0,0 +1,3 @@ +.foo { + prop: val; +} diff --git a/test/less/plugin-preeval.less b/test/less/plugin-preeval.less new file mode 100644 index 000000000..db491fdb2 --- /dev/null +++ b/test/less/plugin-preeval.less @@ -0,0 +1,15 @@ +@plugin "./plugin/plugin-preeval"; + +.two(@rules: {}) { + :root.two & { + @rules(); + } +} + +.one { + .two({ + --foo: foo; + }); +} + +@stop: end; \ No newline at end of file diff --git a/test/less/plugin/plugin-preeval.js b/test/less/plugin/plugin-preeval.js new file mode 100644 index 000000000..8d6386262 --- /dev/null +++ b/test/less/plugin/plugin-preeval.js @@ -0,0 +1,50 @@ +module.exports = { + install({ tree: { Quoted }, visitors }, manager) { + global.TRACK = false + class Visitor { + constructor() { + this.native = new visitors.Visitor(this); + + this.isPreEvalVisitor = true; + this.isReplacing = true; + } + + run(root) { + return this.native.visit(root); + } + + visitDeclaration(node) { + if (node.name === '@stop') { + global.TRACK = false; + } + console.log(node.name, node.value.type); + return node; + } + + visitRuleset(node) { + if (global.TRACK) { + // console.log(node.rules[0]); + } + return node; + } + + visitMixinCall(node) { + console.log('mixin call'); // , node.arguments[0].value.ruleset.rules[0].value.value[0] + global.TRACK = true; + return node; + } + + visitVariable(node) { + // console.log(node.name); + if (node.name === '@foo') { + console.log('visited var'); + return new Quoted(`'`, 'bar', true); + } + return node; + } + } + + manager.addVisitor(new Visitor()); + // console.log(manager); + } +}; From 66cd4026f4f59528b1e580b650961759d8f4d317 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sat, 23 Jun 2018 09:05:48 -0700 Subject: [PATCH 2/4] WIP Allow ES6 in tests --- test/.eslintrc.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/.eslintrc.json diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 000000000..2be2e7911 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "env": { + "node": true, + "browser": true + }, + "parserOptions": { + "ecmaVersion": 6 + } +} From 983fa992b840cc05a84e0b53bc239aa8d60a4861 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sun, 24 Jun 2018 13:01:30 -0700 Subject: [PATCH 3/4] Fixes #3205, partial 3.0 math regression #1880 --- lib/less/parser/parser-input.js | 4 + lib/less/parser/parser.js | 105 ++++++++++++------ lib/less/tree/expression.js | 3 + test/css/permissive-parse.css | 7 +- test/css/plugin-preeval.css | 4 +- .../less/errors/at-rules-unmatching-block.txt | 7 +- test/less/media.less | 2 +- test/less/permissive-parse.less | 5 +- test/less/plugin-preeval.less | 2 +- test/less/plugin/plugin-preeval.js | 26 +---- 10 files changed, 88 insertions(+), 77 deletions(-) diff --git a/lib/less/parser/parser-input.js b/lib/less/parser/parser-input.js index b240f1d4f..e86767b64 100644 --- a/lib/less/parser/parser-input.js +++ b/lib/less/parser/parser-input.js @@ -323,6 +323,10 @@ module.exports = function() { return input.charAt(parserInput.i); }; + parserInput.prevChar = function() { + return input.charAt(parserInput.i - 1); + }; + parserInput.getInput = function() { return input; }; diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index b935461ed..99203e383 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -1293,7 +1293,7 @@ var Parser = function Parser(context, imports, fileInfo) { // Custom property values get permissive parsing if (name[0].value && name[0].value.slice(0, 2) === '--') { - value = this.permissiveValue(';'); + value = this.permissiveValue(); } // Try to store values as anonymous // If we need the value later we'll re-parse it in ruleset.parseValue @@ -1309,13 +1309,12 @@ var Parser = function Parser(context, imports, fileInfo) { if (!value) { value = this.value(); } - - important = this.important(); - - // As a last resort, let a variable try to be parsed as a permissive value + // As a last resort, try permissiveValue if (!value && isVariable) { - value = this.permissiveValue(';'); + value = this.permissiveValue(); } + + important = this.important(); } if (value && this.end()) { @@ -1337,13 +1336,31 @@ var Parser = function Parser(context, imports, fileInfo) { } }, /** - * Used for custom properties and custom at-rules + * Used for custom properties, at-rules, and variables (as fallback) * Parses almost anything inside of {} [] () "" blocks * until it reaches outer-most tokens. + * + * First, it will try to parse comments and entities to reach + * the end. This is mostly like the Expression parser except no + * math is allowed. */ permissiveValue: function (untilTokens) { - var i, e, index = parserInput.i, value = []; - + var i, e, done, value, + tok = untilTokens || ';', + index = parserInput.i, result = []; + + function testCurrentChar() { + var char = parserInput.currentChar(); + if (typeof tok === 'string') { + return char === tok; + } else { + return tok.test(char); + } + } + if (testCurrentChar()) { + return; + } + value = []; do { e = this.comment(); if (e) { @@ -1356,41 +1373,55 @@ var Parser = function Parser(context, imports, fileInfo) { } } while (e); + done = testCurrentChar(); + if (value.length > 0) { - return new(tree.Expression)(value); + value = new(tree.Expression)(value); + if (done) { + return value; + } + else { + result.push(value); + } + // Preserve space before $parseUntil as it will not + if (parserInput.prevChar() === ' ') { + result.push(new tree.Anonymous(' ', index)); + } } - else { - value = parserInput.$parseUntil(untilTokens); + parserInput.save(); + + value = parserInput.$parseUntil(tok); - if (value) { - if (typeof value === 'string') { - error("Expected '" + value + "'", "Parse"); - } - if (value.length === 1 && value[0] === ' ') { - return new tree.Anonymous('', index); + if (value) { + if (typeof value === 'string') { + error("Expected '" + value + "'", "Parse"); + } + if (value.length === 1 && value[0] === ' ') { + parserInput.forget(); + return new tree.Anonymous('', index); + } + var item; + for (i = 0; i < value.length; i++) { + item = value[i]; + if (Array.isArray(item)) { + // Treat actual quotes as normal quoted values + result.push(new tree.Quoted(item[0], item[1], true, index, fileInfo)); } - var item, args = []; - for (i = 0; i < value.length; i++) { - item = value[i]; - if (Array.isArray(item)) { - // Treat actual quotes as normal quoted values - args.push(new tree.Quoted(item[0], item[1], true, index, fileInfo)); - } - else { - if (i === value.length - 1) { - item = item.trim(); - } - // Treat like quoted values, but replace vars like unquoted expressions - var quote = new tree.Quoted("'", item, true, index, fileInfo); - quote.variableRegex = /@([\w-]+)/g; - quote.propRegex = /\$([\w-]+)/g; - quote.reparse = true; - args.push(quote); + else { + if (i === value.length - 1) { + item = item.trim(); } + // Treat like quoted values, but replace vars like unquoted expressions + var quote = new tree.Quoted("'", item, true, index, fileInfo); + quote.variableRegex = /@([\w-]+)/g; + quote.propRegex = /\$([\w-]+)/g; + result.push(quote); } - return new tree.Expression(args, true); } + parserInput.forget(); + return new tree.Expression(result, true); } + parserInput.restore(); }, // @@ -1472,7 +1503,7 @@ var Parser = function Parser(context, imports, fileInfo) { nodes.push(e); } else if (parserInput.$char('(')) { p = this.property(); - e = this.value(); + e = this.permissiveValue(')'); if (parserInput.$char(')')) { if (p && e) { nodes.push(new(tree.Paren)(new(tree.Declaration)(p, e, null, null, parserInput.i, fileInfo, true))); diff --git a/lib/less/tree/expression.js b/lib/less/tree/expression.js index 257c93e3a..fa380ec90 100644 --- a/lib/less/tree/expression.js +++ b/lib/less/tree/expression.js @@ -23,6 +23,9 @@ Expression.prototype.eval = function (context) { } if (this.value.length > 1) { returnValue = new Expression(this.value.map(function (e) { + if (!e.eval) { + return e; + } return e.eval(context); }), this.noSpacing); } else if (this.value.length === 1) { diff --git a/test/css/permissive-parse.css b/test/css/permissive-parse.css index 1ed0773d4..ddb814020 100644 --- a/test/css/permissive-parse.css +++ b/test/css/permissive-parse.css @@ -12,8 +12,8 @@ basically anything until final semi-colon; even other stuff; // i\'m serious; }; - --custom-color: #ff3333; - custom-color: #ff3333; + --custom-color: #ff3333 #ff3333; + custom-color: #ff3333 #ff3333; } .var { --fortran: read (*, *, iostat=1) radius, height; @@ -30,7 +30,6 @@ } } .test-comment { - --value: ; + --value: a /* { ; } */; --comment-within: ( /* okay?; comment; */ ); - --empty: ; } diff --git a/test/css/plugin-preeval.css b/test/css/plugin-preeval.css index f43d8a9cd..920ee35ff 100644 --- a/test/css/plugin-preeval.css +++ b/test/css/plugin-preeval.css @@ -1,3 +1,3 @@ -.foo { - prop: val; +:root.two .one { + --foo: bar !important; } diff --git a/test/less/errors/at-rules-unmatching-block.txt b/test/less/errors/at-rules-unmatching-block.txt index 5070aaf57..b0b50ff0d 100644 --- a/test/less/errors/at-rules-unmatching-block.txt +++ b/test/less/errors/at-rules-unmatching-block.txt @@ -1,4 +1,3 @@ -SyntaxError: @unknown rule is missing block or ending semi-colon in {path}at-rules-unmatching-block.less on line 2, column 10: -1 -2 @unknown url( { -3 50% {width: 20px;} +SyntaxError: expected ')' got '' in {path}at-rules-unmatching-block.less on line 5, column 1: +4 } +5 diff --git a/test/less/media.less b/test/less/media.less index ec90a6618..dd05f0a57 100644 --- a/test/less/media.less +++ b/test/less/media.less @@ -23,7 +23,7 @@ @ratio_large: 16; @ratio_small: 9; -@media all and (device-aspect-ratio: ~"@{ratio_large} / @{ratio_small}") { +@media all and (device-aspect-ratio: @ratio_large / @ratio_small) { body { max-width: 800px; } } diff --git a/test/less/permissive-parse.less b/test/less/permissive-parse.less index f70660354..db2909ba7 100644 --- a/test/less/permissive-parse.less +++ b/test/less/permissive-parse.less @@ -17,7 +17,7 @@ }; --that: @this; @red: lighten(red, 10%); - --custom-color: @red; + --custom-color: @red lighten(red, 10%); custom-color: $--custom-color; } @@ -45,7 +45,6 @@ } // @todo - fix comment absorption after property .test-comment { - --value: /* { ; } */; + --value: a/* { ; } */; --comment-within: ( /* okay?; comment; */ ); - --empty: ; } \ No newline at end of file diff --git a/test/less/plugin-preeval.less b/test/less/plugin-preeval.less index db491fdb2..62d61b20f 100644 --- a/test/less/plugin-preeval.less +++ b/test/less/plugin-preeval.less @@ -8,7 +8,7 @@ .one { .two({ - --foo: foo; + --foo: @replace !important; }); } diff --git a/test/less/plugin/plugin-preeval.js b/test/less/plugin/plugin-preeval.js index 8d6386262..484a0abba 100644 --- a/test/less/plugin/plugin-preeval.js +++ b/test/less/plugin/plugin-preeval.js @@ -1,6 +1,5 @@ module.exports = { install({ tree: { Quoted }, visitors }, manager) { - global.TRACK = false class Visitor { constructor() { this.native = new visitors.Visitor(this); @@ -13,31 +12,8 @@ module.exports = { return this.native.visit(root); } - visitDeclaration(node) { - if (node.name === '@stop') { - global.TRACK = false; - } - console.log(node.name, node.value.type); - return node; - } - - visitRuleset(node) { - if (global.TRACK) { - // console.log(node.rules[0]); - } - return node; - } - - visitMixinCall(node) { - console.log('mixin call'); // , node.arguments[0].value.ruleset.rules[0].value.value[0] - global.TRACK = true; - return node; - } - visitVariable(node) { - // console.log(node.name); - if (node.name === '@foo') { - console.log('visited var'); + if (node.name === '@replace') { return new Quoted(`'`, 'bar', true); } return node; From 5b3bc274a615d40914532d24f67128f4159cc0a3 Mon Sep 17 00:00:00 2001 From: Matthew Dean Date: Sun, 24 Jun 2018 13:02:37 -0700 Subject: [PATCH 4/4] Remove accidentally-committed VSCode files --- .vscode/launch.json | 19 ------------------- .vscode/tasks.json | 9 --------- 2 files changed, 28 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e36b66050..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Less Test (Grunt)", - "program": "${workspaceFolder}/node_modules/grunt/lib/grunt/cli.js", - "cwd": "${workspaceFolder}", - "console":"integratedTerminal", - "args": [ - "test" - ] - } - ] -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 60702afa9..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "0.1.0", - "command": "grunt", - "isShellCommand": true, - "args": ["test"], - "showOutput": "always" -} \ No newline at end of file