From fde62d7196967fd28dc36dc231852adad09f6a96 Mon Sep 17 00:00:00 2001 From: Jan Nicklas Date: Sat, 2 Nov 2024 17:46:31 +0100 Subject: [PATCH] feat: add `global` and `local` for animations (#76) * add global for animations * refactor: logic --------- Co-authored-by: alexander.akait --- README.md | 26 +++++++++ src/index.js | 142 ++++++++++++++++++++++++--------------------- test/index.test.js | 62 ++++++++++++++++++++ 3 files changed, 164 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 29f4534..5501230 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Transformation examples: +Selectors (mode `local`, by default):: + ```css .foo { ... } /* => */ :local(.foo) { ... } @@ -28,6 +30,30 @@ Transformation examples: ``` +Declarations (mode `local`, by default): + + +```css +.foo { + animation-name: fadeInOut, global(moveLeft300px), local(bounce); +} + +.bar { + animation: rotate 1s, global(spin) 3s, local(fly) 6s; +} + +/* => */ + +:local(.foo) { + animation-name: :local(fadeInOut), moveLeft300px, :local(bounce); +} + +:local(.bar) { + animation: :local(rotate) 1s, spin 3s, :local(fly) 6s; +} +``` + + ## Building ```bash diff --git a/src/index.js b/src/index.js index eeaef93..5bad19a 100644 --- a/src/index.js +++ b/src/index.js @@ -347,24 +347,20 @@ function localizeDeclarationValues(localize, declaration, context) { declaration.value = valueNodes.toString(); } -function localizeDeclaration(declaration, context) { - const isAnimation = /animation$/i.test(declaration.prop); - - if (isAnimation) { - // letter - // An uppercase letter or a lowercase letter. - // - // ident-start code point - // A letter, a non-ASCII code point, or U+005F LOW LINE (_). - // - // ident code point - // An ident-start code point, a digit, or U+002D HYPHEN-MINUS (-). - - // We don't validate `hex digits`, because we don't need it, it is work of linters. - const validIdent = - /^-?([a-z\u0080-\uFFFF_]|(\\[^\r\n\f])|-(?![0-9]))((\\[^\r\n\f])|[a-z\u0080-\uFFFF_0-9-])*$/i; - - /* +// letter +// An uppercase letter or a lowercase letter. +// +// ident-start code point +// A letter, a non-ASCII code point, or U+005F LOW LINE (_). +// +// ident code point +// An ident-start code point, a digit, or U+002D HYPHEN-MINUS (-). + +// We don't validate `hex digits`, because we don't need it, it is work of linters. +const validIdent = + /^-?([a-z\u0080-\uFFFF_]|(\\[^\r\n\f])|-(?![0-9]))((\\[^\r\n\f])|[a-z\u0080-\uFFFF_0-9-])*$/i; + +/* The spec defines some keywords that you can use to describe properties such as the timing function. These are still valid animation names, so as long as there is a property that accepts a keyword, it is given priority. Only when all the properties that can take a keyword are @@ -375,38 +371,43 @@ function localizeDeclaration(declaration, context) { The animation will repeat an infinite number of times from the first argument, and will have an animation name of infinite from the second. */ - const animationKeywords = { - // animation-direction - $normal: 1, - $reverse: 1, - $alternate: 1, - "$alternate-reverse": 1, - // animation-fill-mode - $forwards: 1, - $backwards: 1, - $both: 1, - // animation-iteration-count - $infinite: 1, - // animation-play-state - $paused: 1, - $running: 1, - // animation-timing-function - $ease: 1, - "$ease-in": 1, - "$ease-out": 1, - "$ease-in-out": 1, - $linear: 1, - "$step-end": 1, - "$step-start": 1, - // Special - $none: Infinity, // No matter how many times you write none, it will never be an animation name - // Global values - $initial: Infinity, - $inherit: Infinity, - $unset: Infinity, - $revert: Infinity, - "$revert-layer": Infinity, - }; +const animationKeywords = { + // animation-direction + $normal: 1, + $reverse: 1, + $alternate: 1, + "$alternate-reverse": 1, + // animation-fill-mode + $forwards: 1, + $backwards: 1, + $both: 1, + // animation-iteration-count + $infinite: 1, + // animation-play-state + $paused: 1, + $running: 1, + // animation-timing-function + $ease: 1, + "$ease-in": 1, + "$ease-out": 1, + "$ease-in-out": 1, + $linear: 1, + "$step-end": 1, + "$step-start": 1, + // Special + $none: Infinity, // No matter how many times you write none, it will never be an animation name + // Global values + $initial: Infinity, + $inherit: Infinity, + $unset: Infinity, + $revert: Infinity, + "$revert-layer": Infinity, +}; + +function localizeDeclaration(declaration, context) { + const isAnimation = /animation(-name)?$/i.test(declaration.prop); + + if (isAnimation) { let parsedAnimationKeywords = {}; const valueNodes = valueParser(declaration.value).walk((node) => { // If div-token appeared (represents as comma ','), a possibility of an animation-keywords should be reflesh. @@ -414,9 +415,28 @@ function localizeDeclaration(declaration, context) { parsedAnimationKeywords = {}; return; - } - // Do not handle nested functions - else if (node.type === "function") { + } else if ( + node.type === "function" && + node.value.toLowerCase() === "local" && + node.nodes.length === 1 + ) { + node.type = "word"; + node.value = node.nodes[0].value; + + return localizeDeclNode(node, { + options: context.options, + global: context.global, + localizeNextItem: true, + localAliasMap: context.localAliasMap, + }); + } else if (node.type === "function") { + // replace `animation: global(example)` with `animation-name: example` + if (node.value.toLowerCase() === "global" && node.nodes.length === 1) { + node.type = "word"; + node.value = node.nodes[0].value; + } + + // Do not handle nested functions return false; } // Ignore all except word @@ -443,14 +463,12 @@ function localizeDeclaration(declaration, context) { } } - const subContext = { + return localizeDeclNode(node, { options: context.options, global: context.global, localizeNextItem: shouldParseAnimationName && !context.global, localAliasMap: context.localAliasMap, - }; - - return localizeDeclNode(node, subContext); + }); }); declaration.value = valueNodes.toString(); @@ -458,15 +476,7 @@ function localizeDeclaration(declaration, context) { return; } - const isAnimationName = /animation(-name)?$/i.test(declaration.prop); - - if (isAnimationName) { - return localizeDeclarationValues(true, declaration, context); - } - - const hasUrl = /url\(/i.test(declaration.value); - - if (hasUrl) { + if (/url\(/i.test(declaration.value)) { return localizeDeclarationValues(false, declaration, context); } } diff --git a/test/index.test.js b/test/index.test.js index f7d0e81..874634b 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -164,6 +164,11 @@ const tests = [ input: ".foo { animation-name: bar; }", expected: ":local(.foo) { animation-name: :local(bar); }", }, + { + name: "localize a single animation-name #2", + input: ".foo { animation-name: local(bar); }", + expected: ":local(.foo) { animation-name: :local(bar); }", + }, { name: "not localize animation-name in a var function", input: ".foo { animation-name: var(--bar); }", @@ -179,6 +184,63 @@ const tests = [ input: ".foo { animation-name: env(bar); }", expected: ":local(.foo) { animation-name: env(bar); }", }, + { + name: "not localize animation-name in an global function", + input: ".foo { animation-name: global(bar); }", + expected: ":local(.foo) { animation-name: bar; }", + }, + { + name: "localize and not localize animation-name in mixed case", + input: + ".foo { animation-name: fadeInOut, global(moveLeft300px), local(bounce); }", + expected: + ":local(.foo) { animation-name: :local(fadeInOut), moveLeft300px, :local(bounce); }", + }, + { + name: "localize and not localize animation-name in mixed case #2", + options: { mode: "global" }, + input: + ".foo { animation-name: fadeInOut, global(moveLeft300px), local(bounce); }", + expected: + ".foo { animation-name: fadeInOut, moveLeft300px, :local(bounce); }", + }, + { + name: "localize and not localize animation-name in mixed case #3", + options: { mode: "pure" }, + input: + ".foo { animation-name: fadeInOut, global(moveLeft300px), local(bounce); }", + expected: + ":local(.foo) { animation-name: :local(fadeInOut), moveLeft300px, :local(bounce); }", + }, + { + name: "not localize animation in an global function", + input: ".foo { animation: global(bar); }", + expected: ":local(.foo) { animation: bar; }", + }, + { + name: "not localize a certain animation in an global function", + input: ".foo { animation: global(bar), foo; }", + expected: ":local(.foo) { animation: bar, :local(foo); }", + }, + { + name: "localize and not localize a certain animation in mixed case", + input: ".foo { animation: rotate 1s, global(spin) 3s, local(fly) 6s; }", + expected: + ":local(.foo) { animation: :local(rotate) 1s, spin 3s, :local(fly) 6s; }", + }, + { + name: "localize and not localize a certain animation in mixed case #2", + options: { mode: "global" }, + input: ".foo { animation: rotate 1s, global(spin) 3s, local(fly) 6s; }", + expected: ".foo { animation: rotate 1s, spin 3s, :local(fly) 6s; }", + }, + { + name: "localize and not localize a certain animation in mixed case #2", + options: { mode: "pure" }, + input: ".foo { animation: rotate 1s, global(spin) 3s, local(fly) 6s; }", + expected: + ":local(.foo) { animation: :local(rotate) 1s, spin 3s, :local(fly) 6s; }", + }, { name: "not localize animation-name in an env function #2", input: ".foo { animation-name: eNv(bar); }",