From 7dccc8f918e6f7605de057cc32317bbfdcaf2d1b Mon Sep 17 00:00:00 2001 From: Daniel Puckowski Date: Mon, 25 Nov 2024 18:09:02 -0500 Subject: [PATCH] fix(issue:4267) support starting-style at-rule * Add support for ```@starting-style``` at-rule now that browser support is broader. --- packages/less/src/less/parser/parser.js | 35 ++++- packages/less/src/less/tree/atrule-syntax.js | 4 + packages/less/src/less/tree/index.js | 3 +- packages/less/src/less/tree/starting-style.js | 130 ++++++++++++++++++ .../less/src/less/visitors/import-visitor.js | 6 + .../less/visitors/join-selector-visitor.js | 10 ++ .../test-data/css/_main/starting-style.css | 41 ++++++ .../test-data/less/_main/starting-style.less | 50 +++++++ 8 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 packages/less/src/less/tree/starting-style.js create mode 100644 packages/test-data/css/_main/starting-style.css create mode 100644 packages/test-data/less/_main/starting-style.less diff --git a/packages/less/src/less/parser/parser.js b/packages/less/src/less/parser/parser.js index 59aa2eef4..5e8e4f921 100644 --- a/packages/less/src/less/parser/parser.js +++ b/packages/less/src/less/parser/parser.js @@ -4,7 +4,7 @@ import visitors from '../visitors'; import getParserInput from './parser-input'; import * as utils from '../utils'; import functionRegistry from '../functions/function-registry'; -import { ContainerSyntaxOptions, MediaSyntaxOptions } from '../tree/atrule-syntax'; +import { ContainerSyntaxOptions, MediaSyntaxOptions, StartingStyleSyntaxOptions } from '../tree/atrule-syntax'; // // less.js - parser @@ -1864,6 +1864,33 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { return features.length > 0 ? features : null; }, + prepareStartingStyleAtRule: function(index, debugInfo) { + const rules = []; + + if (parserInput.$re(/.*{/)) { + let e; + + while (e = this.declaration()) { + rules.push(e); + } + } + if (rules.length === 0) { + parserInput.restore(); + + return this.prepareAndGetNestableAtRule(tree.StartingStyle, index, debugInfo, StartingStyleSyntaxOptions); + } else if (parserInput.$char('}')) { + const atRule = new (tree.StartingStyle)(rules, [], index + currentIndex, fileInfo); + + if (context.dumpLineNumbers) { + atRule.debugInfo = debugInfo; + } + + return atRule; + } else { + error('starting-style definitions require declarations or rulesets'); + } + }, + prepareAndGetNestableAtRule: function (treeType, index, debugInfo, syntaxOptions) { const features = this.mediaFeatures(syntaxOptions); @@ -1896,10 +1923,12 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) { if (parserInput.$str('@media')) { return this.prepareAndGetNestableAtRule(tree.Media, index, debugInfo, MediaSyntaxOptions); } - - if (parserInput.$str('@container')) { + else if (parserInput.$str('@container')) { return this.prepareAndGetNestableAtRule(tree.Container, index, debugInfo, ContainerSyntaxOptions); } + else if (parserInput.$str('@starting-style')) { + return this.prepareStartingStyleAtRule(index, debugInfo); + } } parserInput.restore(); diff --git a/packages/less/src/less/tree/atrule-syntax.js b/packages/less/src/less/tree/atrule-syntax.js index 0c5decb83..4420da90c 100644 --- a/packages/less/src/less/tree/atrule-syntax.js +++ b/packages/less/src/less/tree/atrule-syntax.js @@ -5,3 +5,7 @@ export const MediaSyntaxOptions = { export const ContainerSyntaxOptions = { queryInParens: true }; + +export const StartingStyleSyntaxOptions = { + queryInParens: false +}; diff --git a/packages/less/src/less/tree/index.js b/packages/less/src/less/tree/index.js index 1d4fbfdd7..fff538dca 100644 --- a/packages/less/src/less/tree/index.js +++ b/packages/less/src/less/tree/index.js @@ -34,6 +34,7 @@ import Negative from './negative'; import Extend from './extend'; import VariableCall from './variable-call'; import NamespaceValue from './namespace-value'; +import StartingStyle from './starting-style'; // mixins import MixinCall from './mixin-call'; @@ -47,7 +48,7 @@ export default { Comment, Anonymous, Value, JavaScript, Assignment, Condition, Paren, Media, Container, QueryInParens, UnicodeDescriptor, Negative, Extend, VariableCall, - NamespaceValue, + NamespaceValue, StartingStyle, mixin: { Call: MixinCall, Definition: MixinDefinition diff --git a/packages/less/src/less/tree/starting-style.js b/packages/less/src/less/tree/starting-style.js new file mode 100644 index 000000000..68facc44d --- /dev/null +++ b/packages/less/src/less/tree/starting-style.js @@ -0,0 +1,130 @@ +import Value from './value'; +import Selector from './selector'; +import AtRule from './atrule'; +import NestableAtRulePrototype from './nested-at-rule'; +import Anonymous from './anonymous'; +import Expression from './expression'; +import Ruleset from './ruleset'; + +const StartingStyle = function(value, features, index, currentFileInfo, visibilityInfo) { + this._index = index; + this._fileInfo = currentFileInfo; + var selectors = (new Selector([], null, null, this._index, this._fileInfo)).createEmptySelectors(); + this.simpleBlock = features && features[0] instanceof Expression === false; + + if (this.simpleBlock) { + this.features = new Value(features); + this.declarations = value; + this.allowRoot = true; + + this.setParent(selectors, this); + this.setParent(this.features, this); + this.setParent(this.declarations, this); + } else { + this.features = new Value([]); + this.rules = [new Ruleset(selectors, value)];//value; + this.rules[0].allowImports = true; + this.allowRoot = true; + + this.setParent(selectors, this); + this.setParent(this.features, this); + this.setParent(this.rules, this); + } + + this.copyVisibilityInfo(visibilityInfo); + +}; + +StartingStyle.prototype = Object.assign(new AtRule(), { + type: 'StartingStyle', + + ...NestableAtRulePrototype, + + genCSS(context, output) { + output.add('@starting-style', this._fileInfo, this._index); + context.firstSelector = true; + + this.features.genCSS(context, output); + + if (this.simpleBlock) { + this.outputRuleset(context, output, this.declarations); + } else { + this.outputRuleset(context, output, this.rules); + } + }, + + eval(context) { + if (!context.mediaBlocks) { + context.mediaBlocks = []; + context.mediaPath = []; + } + + const media = new StartingStyle(null, [], this._index, this._fileInfo, this.visibilityInfo()); + + if (this.simpleBlock) { + if (this.debugInfo) { + this.declarations[0].debugInfo = this.debugInfo; + media.debugInfo = this.debugInfo; + } + + media.features = this.features.eval(context); + + this.declarations[0].functionRegistry = context.frames[0].functionRegistry.inherit(); + context.frames.unshift(this.declarations[0]); + media.declarations = this.declarations.map(rule => rule.eval(context)); + + context.frames.shift(); + + return context.mediaPath.length == 0 ? media.evalTop(context) : + media.evalNestedBlock(context); + } else { + media.simpleBlock = false; + + if (this.debugInfo) { + this.rules[0].debugInfo = this.debugInfo; + media.debugInfo = this.debugInfo; + } + + media.features = this.features.eval(context); + + context.mediaPath.push(media); + context.mediaBlocks.push(media); + + this.rules[0].functionRegistry = context.frames[0].functionRegistry.inherit(); + context.frames.unshift(this.rules[0]); + media.rules = [this.rules[0].eval(context)]; + + context.frames.shift(); + context.mediaPath.pop(); + + return context.mediaPath.length === 0 ? media.evalTop(context) : + media.evalNested(context); + } + }, + + evalNestedBlock: function (context) { + let i; + let value; + let path = context.mediaPath.concat([this]); + + for (i = 0; i < path.length; i++) { + value = path[i].features instanceof Value ? + path[i].features.value : path[i].features; + path[i] = Array.isArray(value) ? value : [value]; + } + + this.features = new Value(this.permute(path).map(function (path) { + path = path.map(function (fragment) { return fragment.toCSS ? fragment : new Anonymous(fragment); }); + for (i = path.length - 1; i > 0; i--) { + path.splice(i, 0, new Anonymous('and')); + } + return new Expression(path); + })); + + this.setParent(this.features, this); + + return new StartingStyle(this.declarations, this.features, this._index, this._fileInfo, this.visibilityInfo()); + }, +}); + +export default StartingStyle; diff --git a/packages/less/src/less/visitors/import-visitor.js b/packages/less/src/less/visitors/import-visitor.js index 29a661bff..aca37f615 100644 --- a/packages/less/src/less/visitors/import-visitor.js +++ b/packages/less/src/less/visitors/import-visitor.js @@ -188,6 +188,12 @@ ImportVisitor.prototype = { }, visitMediaOut: function (mediaNode) { this.context.frames.shift(); + }, + visitStartingStyle: function (mediaNode, visitArgs) { + this.context.frames.unshift(mediaNode.declarations ? mediaNode.declarations[0] : mediaNode.rules[0]); + }, + visitStartingStyleOut: function (mediaNode) { + this.context.frames.shift(); } }; export default ImportVisitor; diff --git a/packages/less/src/less/visitors/join-selector-visitor.js b/packages/less/src/less/visitors/join-selector-visitor.js index 1dd47e904..9c703286a 100644 --- a/packages/less/src/less/visitors/join-selector-visitor.js +++ b/packages/less/src/less/visitors/join-selector-visitor.js @@ -56,6 +56,16 @@ class JoinSelectorVisitor { atRuleNode.rules[0].root = (atRuleNode.isRooted || context.length === 0 || null); } } + + visitStartingStyle(mediaNode, visitArgs) { + let context = this.contexts[this.contexts.length - 1]; + + if (mediaNode.declarations) { + mediaNode.declarations[0].root = (context.length === 0 || context[0].multiMedia); + } else { + mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia); + } + } } export default JoinSelectorVisitor; diff --git a/packages/test-data/css/_main/starting-style.css b/packages/test-data/css/_main/starting-style.css new file mode 100644 index 000000000..7cc0d828b --- /dev/null +++ b/packages/test-data/css/_main/starting-style.css @@ -0,0 +1,41 @@ +#nav { + transition: background-color 3.5s; + background-color: gray; +} +[popover]:popover-open { + opacity: 1; + transform: scaleX(1); + @starting-style { + opacity: 0; + transform: scaleX(0); + } +} +#target { + transition: background-color 1.5s; + background-color: green; +} +@starting-style { + #target { + background-color: transparent; + } +} +#source { + transition: background-color 2.5s; + background-color: red; +} +source:first { + opacity: 1; + transform: scaleX(1); + @starting-style { + opacity: 0; + transform: scaleX(0); + } +} +#footer { + color: yellow; +} +@starting-style { + #footer { + background-color: transparent; + } +} diff --git a/packages/test-data/less/_main/starting-style.less b/packages/test-data/less/_main/starting-style.less new file mode 100644 index 000000000..f43a3f3a4 --- /dev/null +++ b/packages/test-data/less/_main/starting-style.less @@ -0,0 +1,50 @@ +#nav { + transition: background-color 3.5s; + background-color: gray; +} + +[popover]:popover-open { + opacity: 1; + transform: scaleX(1); + + @starting-style { + opacity: 0; + transform: scaleX(0); + } +} + +#target { + transition: background-color 1.5s; + background-color: green; +} + +@starting-style { + #target { + background-color: transparent; + } +} + +#source { + transition: background-color 2.5s; + background-color: red; +} + +source:first { + opacity: 1; + transform: scaleX(1); + + @starting-style { + opacity: 0; + transform: scaleX(0); + } +} + +#footer { + color: yellow; +} + +@starting-style { + #footer { + background-color: transparent; + } +}