├── .npmrc ├── .gitignore ├── lib ├── index.js ├── stringify.js ├── types.d.ts ├── isStyledComponent.js ├── parse.js ├── __tests__ │ ├── isStyledComponent.test.js │ ├── stringify.test.js │ ├── tokenizer.test.js │ ├── parseJs.test.js │ ├── __snapshots__ │ │ └── parseJs.test.js.snap │ └── parse.test.js ├── parseJs.js ├── stringifier.js ├── tokenizer.js └── parser.js ├── .gitattributes ├── .editorconfig ├── tsconfig.json ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── eslint.config.mjs ├── LICENSE.md ├── package.json ├── CHANGELOG.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix = "" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | _dev/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | let parse = require('./parse'); 2 | let stringify = require('./stringify'); 3 | 4 | module.exports = { 5 | parse, 6 | stringify, 7 | }; 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto 3 | 4 | # Force the following filetypes to have unix eols, so Windows does not break them 5 | *.* text eol=lf 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /lib/stringify.js: -------------------------------------------------------------------------------- 1 | let Stringifier = require('./stringifier'); 2 | 3 | /** @type {import('postcss').Stringifier} */ 4 | module.exports = function stringify(node, builder) { 5 | let str = new Stringifier(builder); 6 | 7 | str.stringify(node); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | export type NodeData = { 2 | css: string; 3 | rangeStart: number; 4 | rangeEnd: number; 5 | interpolationRanges: Array<{ 6 | start: number; 7 | end: number; 8 | }>; 9 | locationStart: { 10 | line: number; 11 | column: number; 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "target": "ES2020", 6 | "lib": ["es2020"], 7 | "esModuleInterop": true, 8 | "moduleResolution": "node", 9 | "module": "commonjs", 10 | "noEmit": true, 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "checkJs": true 14 | }, 15 | "exclude": ["./**/node_modules/**/*"], 16 | "include": ["./lib/**/*.js"] 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | lint: 13 | name: Lint on Node.js LTS 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 'lts/*' 23 | 24 | - run: npm ci 25 | 26 | - name: Lint 27 | run: npm run lint 28 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { configs } from 'eslint-config-hudochenkov'; 2 | import eslintConfigPrettier from 'eslint-config-prettier'; 3 | import globals from 'globals'; 4 | 5 | export default [ 6 | ...configs.main, 7 | eslintConfigPrettier, 8 | { 9 | languageOptions: { 10 | globals: { 11 | ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])), 12 | ...globals.node, 13 | ...globals.jest, 14 | groupTest: true, 15 | runTest: true, 16 | }, 17 | }, 18 | rules: { 19 | 'unicorn/prefer-at': 0, 20 | 'no-template-curly-in-string': 0, 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | test: 13 | name: Test on Node.js ${{ matrix.node }} 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | node: [18, 20, 22] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - run: npm ci 30 | 31 | - name: Test 32 | run: npm test 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2023–present Aleks Hudochenkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-styled-syntax", 3 | "version": "0.7.1", 4 | "description": "PostCSS syntax for template literals CSS-in-JS (e. g. styled-components).", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "!lib/__tests__" 9 | ], 10 | "scripts": { 11 | "types": "tsc", 12 | "lint": "eslint . --max-warnings 0 && prettier '**/*.{js,mjs}' --check && npm run types", 13 | "test": "jest", 14 | "watch": "jest --watch", 15 | "coverage": "jest --coverage", 16 | "fix": "eslint . --fix --max-warnings=0 && prettier '**/*.{js,mjs}' --write" 17 | }, 18 | "repository": "hudochenkov/postcss-styled-syntax", 19 | "keywords": [ 20 | "postcss", 21 | "postcss-syntax", 22 | "parser", 23 | "css-in-js", 24 | "styled-components" 25 | ], 26 | "author": "Aleks Hudochenkov ", 27 | "license": "MIT", 28 | "lint-staged": { 29 | "*.{js,mjs}": [ 30 | "eslint --fix --max-warnings 0", 31 | "prettier --write" 32 | ] 33 | }, 34 | "jest": { 35 | "prettierPath": null, 36 | "watchPlugins": [ 37 | "jest-watch-typeahead/filename", 38 | "jest-watch-typeahead/testname" 39 | ] 40 | }, 41 | "prettier": "prettier-config-hudochenkov", 42 | "dependencies": { 43 | "typescript": "^5.7.3" 44 | }, 45 | "peerDependencies": { 46 | "postcss": "^8.5.1" 47 | }, 48 | "devDependencies": { 49 | "@types/jest": "^29.5.14", 50 | "eslint": "^9.18.0", 51 | "eslint-config-hudochenkov": "^11.0.0", 52 | "eslint-config-prettier": "^10.0.1", 53 | "globals": "^15.14.0", 54 | "jest": "^29.7.0", 55 | "jest-watch-typeahead": "^2.2.2", 56 | "postcss": "^8.5.1", 57 | "prettier": "^3.4.2", 58 | "prettier-config-hudochenkov": "^0.4.0" 59 | }, 60 | "engines": { 61 | "node": ">=14.17" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](https://semver.org/). 5 | 6 | ## 0.7.1 7 | 8 | * Improved nodes range detection 9 | 10 | ## 0.7.0 11 | * Added support for passing a function to `styled` instead using it as a tagged template. For example, styled.div(props => \`color: red;\`), or styled(Component)(props => \`color: red;\`) 12 | 13 | ## 0.6.4 14 | * Fixed parsing for CSS with escaped characters 15 | 16 | ## 0.6.3 17 | * Fixed interpolation range if comment after interpolation has a backslash 18 | 19 | ## 0.6.2 20 | * Fixed a JS parsing issue if a comment between a tag function and template literal is present 21 | 22 | ## 0.6.1 23 | * Fixed interpolation ranges if there is a comment inside an interpolation 24 | * Catch more JavaScript parsing errors 25 | 26 | ## 0.6.0 27 | * Use TypeScript instead of @typescript-eslint/typescript-estree for parsing. This fixes “unsupported TypeScript version” messages and reduces install size. 28 | * Fixed parsing for two interpolations before rule selector. Fixes #24 29 | * Dropped support for Node.js 14 and 16 30 | 31 | ## 0.5.0 32 | * Moved `typescript` from `peerDependencies` to `dependencies`. This should also remove “unsupported TypeScript version” messages. Your project doesn't need to be a TypeScript project. `typescript` package is used as a parser for JavaScript and TypeScript files. 33 | 34 | ## 0.4.0 35 | * Added `raws.isRuleLike` to all Roots. Enable PostCSS and Stylelint to adjust to CSS-in-JS quirks. E. g. if something processes only rules, it could also process `root` if this flag is present 36 | 37 | ## 0.3.3 38 | * Fixed: Catch JavaScript parsing errors 39 | 40 | ## 0.3.2 41 | * Fixed stringifier mutating AST 42 | 43 | ## 0.3.1 44 | * Fixed regression for comments inside a selector 45 | 46 | ## 0.3.0 47 | * Interpolation on a separate line before `Rule` now added to `rule.raws.before` instead of being part of a selector 48 | * Fixed at-rule with interpolation before it parsed as a rule 49 | * Fixed parsing error for interpolations before a comment 50 | * Fixed parsing error for multiple interpolations before declaration, while they have no spacing between them 51 | 52 | ## 0.2.0 53 | * Add Node.js 14 support. 54 | 55 | ## 0.1.0 56 | * Initial release. 57 | -------------------------------------------------------------------------------- /lib/isStyledComponent.js: -------------------------------------------------------------------------------- 1 | let ts = require('typescript'); 2 | 3 | /** 4 | * Checkes where it is a styled component or css`` interpolation 5 | * 6 | * @param {ts.Node} node 7 | * @return {boolean} 8 | */ 9 | module.exports.isStyledComponent = function isStyledComponent(node) { 10 | if (ts.isTaggedTemplateExpression(node)) { 11 | // styled.foo`` 12 | if (ts.isPropertyAccessExpression(node.tag)) { 13 | return isStyledIdentifier(node.tag.expression); 14 | } 15 | 16 | if (ts.isCallExpression(node.tag)) { 17 | // styled(Component)`` 18 | if (isStyledIdentifier(node.tag.expression)) { 19 | return true; 20 | } 21 | 22 | // styled.foo.attrs({})`` 23 | if ( 24 | ts.isPropertyAccessExpression(node.tag.expression) && 25 | ts.isPropertyAccessExpression(node.tag.expression.expression) && 26 | isStyledIdentifier(node.tag.expression.expression.expression) 27 | ) { 28 | return true; 29 | } 30 | 31 | // styled(Component).attrs({})`` 32 | if ( 33 | ts.isPropertyAccessExpression(node.tag.expression) && 34 | ts.isCallExpression(node.tag.expression.expression) && 35 | isStyledIdentifier(node.tag.expression.expression.expression) 36 | ) { 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | // css`` or createGlobalStyle`` 44 | if (ts.isIdentifier(node.tag)) { 45 | return node.tag.text === 'css' || node.tag.text === 'createGlobalStyle'; 46 | } 47 | } 48 | 49 | // styled.foo(props => ``) 50 | if ( 51 | ts.isCallExpression(node) && 52 | ts.isPropertyAccessExpression(node.expression) && 53 | isStyledIdentifier(node.expression.expression) && 54 | ts.isArrowFunction(node.arguments[0]) && 55 | (ts.isNoSubstitutionTemplateLiteral(node.arguments[0].body) || 56 | ts.isTemplateExpression(node.arguments[0].body)) 57 | ) { 58 | return true; 59 | } 60 | 61 | // styled(Component)(props => ``) 62 | if ( 63 | ts.isCallExpression(node) && 64 | ts.isCallExpression(node.expression) && 65 | isStyledIdentifier(node.expression.expression) && 66 | ts.isArrowFunction(node.arguments[0]) && 67 | (ts.isNoSubstitutionTemplateLiteral(node.arguments[0].body) || 68 | ts.isTemplateExpression(node.arguments[0].body)) 69 | ) { 70 | return true; 71 | } 72 | 73 | return false; 74 | }; 75 | 76 | /** 77 | * @param {ts.LeftHandSideExpression} expression 78 | * @return {boolean} 79 | */ 80 | function isStyledIdentifier(expression) { 81 | return ts.isIdentifier(expression) && expression.text === 'styled'; 82 | } 83 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | let postcss = require('postcss'); 2 | let Parser = require('./parser'); 3 | const { parseJs } = require('./parseJs'); 4 | 5 | /** @typedef {import('./types.d.ts').NodeData} NodeData */ 6 | 7 | /** @type {import('postcss').Parser} */ 8 | module.exports = function parse(css, opts) { 9 | let inputCode = typeof css === 'string' ? css : css.toString(); 10 | 11 | let options = opts ?? {}; 12 | 13 | options.document = inputCode; 14 | 15 | let document = new postcss.Document({ 16 | source: { 17 | input: new postcss.Input(inputCode, options), 18 | start: { offset: 0, line: 1, column: 1 }, 19 | }, 20 | }); 21 | 22 | /** @type {NodeData[]} */ 23 | let foundNodes = parseJs(inputCode, opts); 24 | 25 | let components = foundNodes.filter((node) => isComponent(node, foundNodes)); 26 | let lastComponent = components[components.length - 1]; 27 | 28 | let previousComponentRangeEnd = 0; 29 | 30 | for (let node of foundNodes) { 31 | /** @type {postcss.Root} */ 32 | let parsedNode; 33 | 34 | let input = new postcss.Input(node.css, options); 35 | 36 | let interpolationsRanges = node.interpolationRanges.map((range) => { 37 | return { 38 | start: range.start - node.rangeStart, 39 | end: range.end - node.rangeStart - 1, 40 | }; 41 | }); 42 | 43 | let parser = new Parser(input, { 44 | interpolations: interpolationsRanges, 45 | isComponent: isComponent(node, foundNodes), 46 | raws: { 47 | originalContent: node.css, 48 | rangeStart: node.rangeStart, 49 | rangeEnd: node.rangeEnd, 50 | locationStart: node.locationStart, 51 | }, 52 | }); 53 | 54 | parser.parse(); 55 | 56 | // @ts-ignore -- Parser types are missing in PostCSS 57 | parsedNode = parser.root; 58 | 59 | if (isComponent(node, foundNodes)) { 60 | parsedNode.raws.codeBefore = inputCode.slice( 61 | previousComponentRangeEnd, 62 | node.rangeStart, 63 | ); 64 | 65 | previousComponentRangeEnd = node.rangeEnd; 66 | 67 | let isLastNode = node.rangeStart === lastComponent?.rangeStart; 68 | 69 | if (isLastNode) { 70 | parsedNode.raws.codeAfter = inputCode.slice(node.rangeEnd); 71 | } 72 | } 73 | 74 | document.append(parsedNode); 75 | } 76 | 77 | return document; 78 | }; 79 | 80 | /** 81 | * Check if it's a standalone component or interpolation within a component 82 | * 83 | * @param {NodeData} node 84 | * @param {NodeData[]} allNodes 85 | * @returns {boolean} 86 | */ 87 | function isComponent(node, allNodes) { 88 | let isSubNode = allNodes.some((item) => { 89 | return item.interpolationRanges.some((range) => { 90 | return range.start < node.rangeStart && node.rangeEnd < range.end; 91 | }); 92 | }); 93 | 94 | return !isSubNode; 95 | } 96 | -------------------------------------------------------------------------------- /lib/__tests__/isStyledComponent.test.js: -------------------------------------------------------------------------------- 1 | let ts = require('typescript'); 2 | let { isStyledComponent } = require('../isStyledComponent'); 3 | 4 | describe('isStyledComponent', () => { 5 | /** 6 | * @param {string} code 7 | */ 8 | function getExpression(code) { 9 | const sourceFile = ts.createSourceFile('temp.ts', code, ts.ScriptTarget.Latest, true); 10 | 11 | // @ts-ignore 12 | const expression = sourceFile.statements[0].expression; 13 | 14 | return expression; 15 | } 16 | 17 | it('allow styled.foo``', () => { 18 | let tag = getExpression('styled.foo``'); 19 | 20 | expect(isStyledComponent(tag)).toBe(true); 21 | }); 22 | 23 | it('allow styled(Component)``', () => { 24 | let tag = getExpression('styled(Component)``'); 25 | 26 | expect(isStyledComponent(tag)).toBe(true); 27 | }); 28 | 29 | it('allow styled.foo.attrs({})``', () => { 30 | let tag = getExpression('styled.foo.attrs({})``'); 31 | 32 | expect(isStyledComponent(tag)).toBe(true); 33 | }); 34 | 35 | it('allow styled(Component).attrs({})``', () => { 36 | let tag = getExpression('styled(Component).attrs({})``'); 37 | 38 | expect(isStyledComponent(tag)).toBe(true); 39 | }); 40 | 41 | it('allow css``', () => { 42 | let tag = getExpression('css``'); 43 | 44 | expect(isStyledComponent(tag)).toBe(true); 45 | }); 46 | 47 | it('allow createGlobalStyle``', () => { 48 | let tag = getExpression('createGlobalStyle``'); 49 | 50 | expect(isStyledComponent(tag)).toBe(true); 51 | }); 52 | 53 | it('disallow other identifiers', () => { 54 | let tag = getExpression('other``'); 55 | 56 | expect(isStyledComponent(tag)).toBe(false); 57 | }); 58 | 59 | it('disallow sstyled.foo``', () => { 60 | let tag = getExpression('sstyled.foo``'); 61 | 62 | expect(isStyledComponent(tag)).toBe(false); 63 | }); 64 | 65 | it('disallow sstyled(Component)``', () => { 66 | let tag = getExpression('sstyled(Component)``'); 67 | 68 | expect(isStyledComponent(tag)).toBe(false); 69 | }); 70 | 71 | it('disallow sstyled.foo.attrs({})``', () => { 72 | let tag = getExpression('sstyled.foo.attrs({})``'); 73 | 74 | expect(isStyledComponent(tag)).toBe(false); 75 | }); 76 | 77 | it('disallow sstyled(Component).attrs({})``', () => { 78 | let tag = getExpression('sstyled(Component).attrs({})``'); 79 | 80 | expect(isStyledComponent(tag)).toBe(false); 81 | }); 82 | 83 | it('allow styled.foo(props => ``)', () => { 84 | let tag = getExpression('styled.foo(props => ``)'); 85 | 86 | expect(isStyledComponent(tag)).toBe(true); 87 | }); 88 | 89 | it('allow styled.foo(({ prop }) => ``)', () => { 90 | let tag = getExpression('styled.foo(({ prop }) => ``)'); 91 | 92 | expect(isStyledComponent(tag)).toBe(true); 93 | }); 94 | 95 | it("allow styled('foo')(props => ``)", () => { 96 | let tag = getExpression("styled('foo')(props => ``)"); 97 | 98 | expect(isStyledComponent(tag)).toBe(true); 99 | }); 100 | 101 | it('allow styled(Component)(props => ``)', () => { 102 | let tag = getExpression('styled(Component)(props => ``)'); 103 | 104 | expect(isStyledComponent(tag)).toBe(true); 105 | }); 106 | 107 | it('disallow conditional return', () => { 108 | let tag = getExpression('styled.foo(({ prop }) => props ? `` : ``)'); 109 | 110 | expect(isStyledComponent(tag)).toBe(false); 111 | }); 112 | 113 | it('disallow explicit return', () => { 114 | let tag = getExpression('styled.foo(({ prop }) => { return ``; })'); 115 | 116 | expect(isStyledComponent(tag)).toBe(false); 117 | }); 118 | 119 | it('disallow return of not template literal', () => { 120 | let tag = getExpression("styled.foo(({ prop }) => '')"); 121 | 122 | expect(isStyledComponent(tag)).toBe(false); 123 | }); 124 | 125 | it('disallow anything else in the function body', () => { 126 | let tag = getExpression('styled.foo(({ prop }) => {})'); 127 | 128 | expect(isStyledComponent(tag)).toBe(false); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /lib/parseJs.js: -------------------------------------------------------------------------------- 1 | let ts = require('typescript'); 2 | let { isStyledComponent } = require('./isStyledComponent'); 3 | 4 | /** @typedef {import('./types.d.ts').NodeData} NodeData */ 5 | 6 | /** 7 | * 8 | * @param {string} inputCode 9 | * @param {import('postcss').ProcessOptions} [opts] 10 | * @returns {NodeData[]} 11 | */ 12 | module.exports.parseJs = function parseJs(inputCode, opts) { 13 | /** @type {NodeData[]} */ 14 | let foundNodes = []; 15 | 16 | try { 17 | let sourceFile = ts.createSourceFile( 18 | opts?.from || 'unnamed.ts', 19 | inputCode, 20 | ts.ScriptTarget.Latest, 21 | // true, 22 | // ts.ScriptKind.TSX, 23 | ); 24 | 25 | /** 26 | * Recursively visits the nodes in the AST 27 | * 28 | * @param {ts.Node} node - The current node in the AST. 29 | */ 30 | 31 | function visit(node) { 32 | // Check if the node is a TaggedTemplateExpression 33 | if (isStyledComponent(node)) { 34 | if (ts.isTaggedTemplateExpression(node)) { 35 | let nodeCssData = getNodeCssData(node.template, inputCode, sourceFile); 36 | 37 | foundNodes.push(nodeCssData); 38 | } 39 | 40 | if ( 41 | ts.isCallExpression(node) && 42 | ts.isArrowFunction(node.arguments[0]) && 43 | (ts.isNoSubstitutionTemplateLiteral(node.arguments[0].body) || 44 | ts.isTemplateExpression(node.arguments[0].body)) 45 | ) { 46 | let nodeCssData = getNodeCssData(node.arguments[0].body, inputCode, sourceFile); 47 | 48 | foundNodes.push(nodeCssData); 49 | } 50 | } 51 | 52 | // Continue recursion down the tree 53 | ts.forEachChild(node, visit); 54 | } 55 | 56 | // @ts-expect-error -- parseDiagnostics is not public API. However, TS is crashing or very-very slow if using official way 57 | // https://github.com/microsoft/TypeScript/issues/21940 58 | let hasParseErrors = sourceFile.parseDiagnostics?.length > 0; 59 | 60 | if (!hasParseErrors) { 61 | visit(sourceFile); 62 | } 63 | } catch { 64 | // Don't show parsing errors for JavaScript/TypeScript, because they are not relevant to CSS. And these errors most likely caught for user by JavaScript tools already 65 | } 66 | 67 | return foundNodes; 68 | }; 69 | 70 | /** 71 | * 72 | * @param {ts.TemplateExpression | ts.NoSubstitutionTemplateLiteral} template 73 | * @param {string} inputCode 74 | * @param {ts.SourceFile} sourceFile 75 | * @return {NodeData} 76 | */ 77 | function getNodeCssData(template, inputCode, sourceFile) { 78 | // TypeScript AST doesn't provide comments, but it count comments towards `pos` and `end` of the node. We have to use creative ways to get true `pos` and `end` of nodes 79 | 80 | /** @type {NodeData["interpolationRanges"]} */ 81 | let interpolationRanges = []; 82 | 83 | if ('templateSpans' in template) { 84 | for (let index = 0; index < template.templateSpans.length; index++) { 85 | let templateSpan = template.templateSpans[index]; 86 | 87 | // To include `${` 88 | let start = templateSpan.pos - 2; 89 | // To include `}` 90 | let end = templateSpan.literal.end - (templateSpan.literal.rawText?.length || 0); 91 | 92 | if (ts.isTemplateTail(templateSpan.literal)) { 93 | end = end - 1; 94 | } else { 95 | // If it's a TemplateMiddle 96 | end = end - 2; 97 | } 98 | 99 | interpolationRanges.push({ start, end }); 100 | } 101 | } 102 | 103 | let text; 104 | 105 | // Template literal without interpolation 106 | if (ts.isNoSubstitutionTemplateLiteral(template)) { 107 | text = template.rawText || ''; 108 | } else { 109 | // If it's a TemplateExpression 110 | text = template.head.rawText || ''; 111 | } 112 | 113 | // exclude backticks 114 | let rangeStart = inputCode.indexOf(text, template.pos + 1); 115 | let rangeEnd = template.end - 1; 116 | 117 | let { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, template.pos); 118 | 119 | return { 120 | css: inputCode.slice(rangeStart, rangeEnd), 121 | interpolationRanges, 122 | rangeStart, 123 | rangeEnd, 124 | // Location is a start of a range, but converted into line and column 125 | locationStart: { 126 | line: line + 1, 127 | column: character + 2, // +1 to start count from 1, and not from 0. Another +1 to include backticks similar to range 128 | }, 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-styled-syntax 2 | 3 | PostCSS syntax for template literals CSS-in-JS (e. g. styled-components, Emotion). It was built to be used as [Stylelint] custom syntax or with [PostCSS] plugins. 4 | 5 | Syntax supports: 6 | 7 | - Full spectrum of styled-components syntax 8 | - Deeply nested interpolations 9 | - Interpolations in selectors, property names, and values 10 | - JavaScript and TypeScript (including files with JSX) 11 | - All functions: 12 | - styled.foo`` 13 | - styled(Component)`` 14 | - styled.foo.attrs({})`` 15 | - styled(Component).attrs({})`` 16 | - styled.foo(props => ``) 17 | - styled(Component)(props => ``) 18 | - css`` 19 | - createGlobalStyle`` 20 | 21 | ```js 22 | let Component = styled.p` 23 | color: #bada55; 24 | `; 25 | ``` 26 | 27 | ## Install 28 | 29 | ``` 30 | npm install --save-dev postcss-styled-syntax 31 | ``` 32 | 33 | ## Usage 34 | 35 | ### Stylelint 36 | 37 | Install syntax and add to a Stylelint config: 38 | 39 | ```json 40 | { 41 | "customSyntax": "postcss-styled-syntax" 42 | } 43 | ``` 44 | 45 | Stylelint [custom syntax documentation](https://stylelint.io/user-guide/usage/options#customsyntax). 46 | 47 | ### PostCSS 48 | 49 | Install syntax and add to a PostCSS config: 50 | 51 | ```js 52 | module.exports = { 53 | syntax: 'postcss-styled-syntax', 54 | plugins: [ /* ... */ ], 55 | }; 56 | ``` 57 | 58 | An example assumes using [PostCSS CLI](https://github.com/postcss/postcss-cli) or another PostCSS runner with config support. 59 | 60 | ## How it works 61 | 62 | ### Parsing 63 | 64 | Syntax parser JavaScript/TypeScript code and find all supported components and functions (e.g., css\`\`). Then, it goes over them and builds a PostCSS AST, where all found components become `Root` nodes inside the `Document` node. 65 | 66 | All interpolations within the found component CSS end up in the AST. E. g. for a declaration `color: ${brand}` `Decl` node will look like this: 67 | 68 | ```js 69 | Decl { 70 | prop: 'color', 71 | value: '${brand}', 72 | } 73 | ``` 74 | 75 | When interpolation is not part of any node, it goes to the next node's `raws.before`. For example, for the following code: 76 | 77 | ```js 78 | let Component = styled.p` 79 | ${textStyles} 80 | 81 | color: red; 82 | `; 83 | ``` 84 | 85 | AST will look like: 86 | 87 | ```js 88 | Decl { 89 | prop: 'color', 90 | value: 'red', 91 | raws: { 92 | before: '\n\t${textStyles}\n\n\t', 93 | // ... 94 | } 95 | } 96 | ``` 97 | 98 | If there is no next node after interpolation, it will go to parents `raws.after`. For example, for the following code: 99 | 100 | ```js 101 | let Component = styled.p` 102 | color: red; 103 | 104 | ${textStyles} 105 | `; 106 | ``` 107 | 108 | AST will look like: 109 | 110 | ```js 111 | Root { 112 | nodes: [ 113 | Decl { 114 | prop: 'color', 115 | value: 'red', 116 | }, 117 | ], 118 | raws: { 119 | after: '\n\n\t${textStyles}\n' 120 | // ... 121 | }, 122 | } 123 | ``` 124 | 125 | ### Stringifying 126 | 127 | Mostly, it works just as the default PostCSS stringifyer. The main difference is the `css` helper in interpolations within a styled component code. E. g. situations like this: 128 | 129 | ```js 130 | let Component = styled.p` 131 | ${(props) => 132 | props.isPrimary 133 | ? css` 134 | background: green; 135 | ` 136 | : css` 137 | border: 1px solid blue; 138 | `} 139 | 140 | color: red; 141 | `; 142 | ``` 143 | 144 | `css` helper inside an interpolation within `Component` code. 145 | 146 | During parsing, the whole interpolation (`${(props) ... }`) is added as `raws.before` to `color: red` node. And it should not be modified. Each `css` helper remembers their original content (as a string). 147 | 148 | When stringifyer reaches a node's `raws.before`, it checks if it has interpolations with `css` helpers. If yes, it searches for the original content of `css` helper and replaces it with a stringified `css` helper. This way, changes to the AST of the `css` helper will be stringified. 149 | 150 | ## Known issues 151 | 152 | - Double-slash comments (`//`) will result in a parsing error. Use standard CSS comments instead (`/* */`). It is definitely possible to add support for double-slash comments, but let's use standard CSS as much as possible 153 | 154 | - Source maps won't work or cannot be trusted. I did not disable them on purpose. But did not test them at all. Because of the way we need to handle `css` helpers within a styled component, `source.end` positions on a node might change if `css` AST changes. See the “How it works” section on stringifying for more info. 155 | 156 | ## Acknowledgements 157 | 158 | [PostCSS] for tokenizer, parser, stringifier, and tests for them. 159 | 160 | [Prettier](https://prettier.io/) for styled-components detection function in an ESTree AST. 161 | 162 | [Stylelint]: https://stylelint.io/ 163 | [PostCSS]: https://postcss.org/ 164 | -------------------------------------------------------------------------------- /lib/__tests__/stringify.test.js: -------------------------------------------------------------------------------- 1 | let stringify = require('../stringify'); 2 | let parse = require('../parse'); 3 | 4 | let tests = [ 5 | 'let Component = styled.div`color: red;`;', 6 | 'let Component = styled.div`color: red;`;\nlet Component = styled.div`border-color: blue`;', 7 | 'let Component = styled.div``;', 8 | 'let Component = styled.div`\n\tcolor: red;\n`;', 9 | 'let Component = styled.div` \n\tcolor: red;\n`;', 10 | 'let Component = styled.div`color: ${red}`;', 11 | 'let Component = styled.div`color: ${red};`;', 12 | 'let Component = styled.div`color: ${red} !important`;', 13 | 'let Component = styled.div`color: ${red} !important;`;', 14 | 'let Component = styled.div`box-shadow: ${elevation1}, ${elevation2}`;', 15 | 'let Component = styled.div`${color}: red`;', 16 | 'let Component = styled.div`display: flex; ${color}: red`;', 17 | 'let Component = styled.div`${Component} { color: red; }`;', 18 | 'let Component = styled.div`${Component}, a { color: red; }`;', 19 | 'let Component = styled.div`a, ${Component} { color: red; }`;', 20 | 'let Component = styled.div`a ${Component} { color: red; }`;', 21 | 'let Component = styled.div`${Component} a { color: red; }`;', 22 | 'let Component = styled.div`${Component}; a { color: red; }`;', 23 | 'let Component = styled.div`color: red; ${borderWidth}`;', 24 | 'let Component = styled.div`color: red; ${borderWidth} `;', 25 | 'let Component = styled.div`color: red; ${borderWidth} ${hello} `;', 26 | 'let Component = styled.div`color: red; ${borderWidth};`;', 27 | 'let Component = styled.div`${borderWidth}color: red`;', 28 | 'let Component = styled.div`${borderWidth} color: red`;', 29 | 'let Component = styled.div`${borderWidth};color: red`;', 30 | 'let Component = styled.div`${borderWidth}; color: red`;', 31 | 'let Component = styled.div`color: red; ${borderWidth} border-color: blue;`;', 32 | 'let Component = styled.div`color: red; ${borderWidth}; border-color: blue;`;', 33 | 'let Component = styled.div`${color}`;', 34 | 'let Component = styled.div`${color};`;', 35 | 'let Component = styled.div`color: red; ${borderWidth} border-color: blue; ${anotherThing} display: none;`;', 36 | 'let Component = styled.div`color: red; ${ borderWidth} border-color: blue; ${anotherThing}`;', 37 | 'let Component = styled.div`color: red; ${borderWidth}${borderWidth}`;', 38 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth} `;', 39 | 'let Component = styled.div`color: red; ${borderWidth};`;', 40 | 'let Component = styled.div`${borderWidth}${borderWidth}color: red`;', 41 | 'let Component = styled.div`${borderWidth} ${borderWidth} color: red`;', 42 | 'let Component = styled.div`${borderWidth}${borderWidth};color: red`;', 43 | 'let Component = styled.div`${borderWidth}${borderWidth}; color: red`;', 44 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth} border-color: blue;`;', 45 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth}; border-color: blue;`;', 46 | 'let Component = styled.div`${color}`;', 47 | 'let Component = styled.div`${color}${color};`;', 48 | 'let Component = styled.div`div{${Component} { color: red; }}`;', 49 | 'let Component = styled.div`div{ ${Component} a { color: red; } }`;', 50 | 'let Component = styled.div`div {${Component}; a { color: red; }}`;', 51 | 'let Component = styled.div`div{color: red; ${borderWidth} }`;', 52 | 'let Component = styled.div`div{color: red; ${borderWidth};}`;', 53 | 'let Component = styled.div`div{${color}}`;', 54 | 'let Component = styled.div`div{${color};}`;', 55 | 'let Component = styled.div`color: green;${css`color: red`}`;', 56 | 'let Component = styled.div`${css`color: red`} color: green;`;', 57 | 'let Component = styled.div`${css`color: red`} color: green;${css`color: blue`}`;', 58 | 'let Component = styled.div`${css`color: red`}${css`color: blue`}`;', 59 | 'let Component = styled.div`${css`color: red;${css`color: blue`}`}`;', 60 | 'let Component = styled.div`${css`color: red;${css`color: blue;${css`color: green;`}`}`}`;', 61 | 'let Component = styled.div`${css`${css`color: blue`} color: red;`}`;', 62 | 'let Component = styled.div`color: green;${props => `color: red`}`;', 63 | 'let Component = styled.div`color: green;${props => css`color: red`}`;', 64 | 'let Component = styled.div`${props => `color: red`} color: green;`;', 65 | 'let Component = styled.div`${props => css`color: red`} color: green;`;', 66 | 'let Component = styled.div<{ isDisabled?: boolean; }>`color: red;`;', 67 | 'let Component = "";', 68 | 'const StyledHeader = styled.thead`\n${css`position: sticky;`}\n`;\nconst Trigger = styled.div``;', 69 | 'let Component = styled.div`a,\n\tb { color: red; }`;', 70 | 'let Component = styled.div`${hello}\n\ta { color: red; }`;', 71 | 'let Component = styled.div`position: sticky', 72 | 'let Component = styled.div(props => `position: sticky;`)', 73 | 'let Component = styled.div(props => `position: ${pos};`)', 74 | ]; 75 | 76 | /** 77 | * @param {string} css 78 | */ 79 | function run(css) { 80 | let root = parse(css); 81 | let output = ''; 82 | 83 | stringify(root, (i) => { 84 | output += i; 85 | }); 86 | 87 | expect(output).toBe(css); 88 | } 89 | 90 | test.each(tests)('%s', (componentCode) => { 91 | run(componentCode); 92 | }); 93 | 94 | test('stringify should not mutate AST', () => { 95 | let document = parse('let Component = styled.div`color: red; ${css`padding: 0;`}`;'); 96 | 97 | expect(document.nodes).toHaveLength(2); 98 | 99 | stringify(document, () => {}); 100 | 101 | expect(document.nodes).toHaveLength(2); 102 | }); 103 | -------------------------------------------------------------------------------- /lib/stringifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let PostCSSStringifier = require('postcss/lib/stringifier').default; 4 | 5 | class Stringifier extends PostCSSStringifier { 6 | /** 7 | * @param {import("postcss").Builder} builder 8 | */ 9 | constructor(builder) { 10 | super(builder); 11 | 12 | // Used to restore builder, which we replace for non-component roots to avoid adding these roots twice 13 | this.originalBuilder = builder; 14 | 15 | // Non-component roots 16 | /** @type {Array<{ start: number; end: number; content: string; originalContent: string; }>} */ 17 | this.stringifiedInterpolations = []; 18 | } 19 | 20 | /** 21 | * @param {import("postcss").Document} node 22 | */ 23 | document(node) { 24 | // If file has no components return it's original content 25 | if (node.nodes.length === 0) { 26 | // @ts-ignore -- it will have source, because parser always sets it 27 | this.builder(node.source.input.css); 28 | 29 | return; 30 | } 31 | 32 | let interpolationRoots = node.nodes.filter((item) => !item.raws.styledSyntaxIsComponent); 33 | 34 | for (let child of interpolationRoots) { 35 | let content = ''; 36 | 37 | this.builder = (/** @type {string} */ item) => { 38 | content += item; 39 | }; 40 | 41 | // @ts-ignore 42 | let semicolon = /** @type {import("postcss").Root["raws"]["semicolon"]} */ ( 43 | this.raw(child, 'semicolon') 44 | ); 45 | 46 | this.stringify(child, semicolon); 47 | 48 | let stringifiedInterpolation = { 49 | start: child.raws.styledSyntaxRangeStart, 50 | end: child.raws.styledSyntaxRangeEnd, 51 | content, 52 | originalContent: child.raws.styledOriginalContent, 53 | }; 54 | 55 | this.stringifiedInterpolations.push(stringifiedInterpolation); 56 | } 57 | 58 | this.builder = this.originalBuilder; 59 | 60 | this.body(node); 61 | } 62 | 63 | /** 64 | * @param {import("postcss").Root} node 65 | */ 66 | root(node) { 67 | if (node.raws.codeBefore) { 68 | this.builder(node.raws.codeBefore); 69 | } 70 | 71 | this.body(node); 72 | 73 | if (node.raws.after) { 74 | node.raws.after = this.replaceInterpolations( 75 | node.raws.after, 76 | node.raws.styledSyntaxRangeEnd, 77 | ); 78 | 79 | this.builder(node.raws.after); 80 | } 81 | 82 | if (node.raws.codeAfter) { 83 | this.builder(node.raws.codeAfter); 84 | } 85 | } 86 | 87 | /** 88 | * @param {import("postcss").Container | import("postcss").Document} node 89 | */ 90 | body(node) { 91 | // STYLED PATCH { 92 | let nodes = node.nodes; 93 | 94 | // Ignore interpolations for a Document node 95 | if (isDocument(node)) { 96 | nodes = node.nodes.filter((item) => item.raws.styledSyntaxIsComponent); 97 | } 98 | 99 | if (!nodes) { 100 | return; 101 | } 102 | // } STYLED PATCH 103 | 104 | let last = nodes.length - 1; 105 | 106 | while (last > 0) { 107 | if (nodes[last].type !== 'comment') break; 108 | 109 | last -= 1; 110 | } 111 | 112 | // @ts-ignore -- PostCSS code 113 | let semicolon = this.raw(node, 'semicolon'); 114 | 115 | // eslint-disable-next-line unicorn/no-for-loop -- disabling for performance reasons 116 | for (let i = 0; i < nodes.length; i++) { 117 | let child = nodes[i]; 118 | 119 | let before = this.raw(child, 'before'); 120 | 121 | if (before) { 122 | // STYLED PATCH { 123 | if (child?.source?.start?.offset !== undefined) { 124 | before = this.replaceInterpolations(before, child.source.start.offset); 125 | } 126 | // } STYLED PATCH 127 | 128 | this.builder(before); 129 | } 130 | 131 | // @ts-ignore -- PostCSS code 132 | this.stringify(child, last !== i || semicolon); 133 | } 134 | } 135 | 136 | /** 137 | * @param {import("postcss").Rule | import("postcss").AtRule} node 138 | * @param {string} start 139 | */ 140 | block(node, start) { 141 | let between = this.raw(node, 'between', 'beforeOpen'); 142 | 143 | this.builder(start + between + '{', node, 'start'); 144 | 145 | let after; 146 | 147 | if (node.nodes && node.nodes.length) { 148 | this.body(node); 149 | after = this.raw(node, 'after'); 150 | } else { 151 | after = this.raw(node, 'after', 'emptyBody'); 152 | } 153 | 154 | if (after) { 155 | // STYLED PATCH { 156 | if (node?.source?.end?.offset !== undefined) { 157 | after = this.replaceInterpolations(after, node.source.end.offset); 158 | } 159 | // } STYLED PATCH 160 | 161 | this.builder(after); 162 | } 163 | 164 | this.builder('}', node, 'end'); 165 | } 166 | 167 | /** 168 | * @param {string} str 169 | * @param {number} strEndOffset 170 | * 171 | * @returns {string} 172 | */ 173 | replaceInterpolations(str, strEndOffset) { 174 | let newStr = str; 175 | 176 | if (newStr.includes('${')) { 177 | let startOfStr = strEndOffset - newStr.length; 178 | let endOfStr = strEndOffset; 179 | 180 | for (let i = 0; i < this.stringifiedInterpolations.length; i++) { 181 | let interpolation = this.stringifiedInterpolations[i]; 182 | 183 | if (startOfStr <= interpolation.start && interpolation.end <= endOfStr) { 184 | newStr = newStr.replace(interpolation.originalContent, interpolation.content); 185 | } 186 | } 187 | } 188 | 189 | return newStr; 190 | } 191 | } 192 | 193 | /** 194 | * @param {import("postcss").Container | import("postcss").Document} node 195 | * 196 | * @returns {node is import("postcss").Document} 197 | */ 198 | function isDocument(node) { 199 | return node.type === 'document'; 200 | } 201 | 202 | module.exports = Stringifier; 203 | -------------------------------------------------------------------------------- /lib/tokenizer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @typedef {[string, string, ...number[]]} Token */ 4 | /** @typedef {{ ignoreErrors?: boolean, interpolations?: Array<{ start: number, end: number }> }} TokenizerOptions */ 5 | 6 | const SINGLE_QUOTE = "'".charCodeAt(0); 7 | const DOUBLE_QUOTE = '"'.charCodeAt(0); 8 | const BACKSLASH = '\\'.charCodeAt(0); 9 | const SLASH = '/'.charCodeAt(0); 10 | const NEWLINE = '\n'.charCodeAt(0); 11 | const SPACE = ' '.charCodeAt(0); 12 | const FEED = '\f'.charCodeAt(0); 13 | const TAB = '\t'.charCodeAt(0); 14 | const CR = '\r'.charCodeAt(0); 15 | const OPEN_SQUARE = '['.charCodeAt(0); 16 | const CLOSE_SQUARE = ']'.charCodeAt(0); 17 | const OPEN_PARENTHESES = '('.charCodeAt(0); 18 | const CLOSE_PARENTHESES = ')'.charCodeAt(0); 19 | const OPEN_CURLY = '{'.charCodeAt(0); 20 | const CLOSE_CURLY = '}'.charCodeAt(0); 21 | const SEMICOLON = ';'.charCodeAt(0); 22 | const ASTERISK = '*'.charCodeAt(0); 23 | const COLON = ':'.charCodeAt(0); 24 | const AT = '@'.charCodeAt(0); 25 | 26 | // STYLED PATCH { 27 | const DOLLAR_SIGN = '$'.charCodeAt(0); 28 | // } STYLED PATCH 29 | 30 | const RE_AT_END = /[\t\n\f\r "#'()/;[\\\]{}]/g; 31 | const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g; 32 | const RE_BAD_BRACKET = /.[\n"'(/\\]/; 33 | const RE_HEX_ESCAPE = /[\da-f]/i; 34 | 35 | /** 36 | * @param {import('postcss').Input} input 37 | * @param {TokenizerOptions} [options] 38 | */ 39 | function tokenizer(input, options = {}) { 40 | let css = input.css.valueOf(); 41 | let ignore = options.ignoreErrors; 42 | 43 | // STYLED PATCH { 44 | let interpolations = options.interpolations || []; 45 | // } STYLED PATCH 46 | 47 | /** @type {number} */ 48 | let code; 49 | /** @type {number} */ 50 | let next; 51 | /** @type {'"'| "'"} */ 52 | let quote; 53 | /** @type {string} */ 54 | let content; 55 | /** @type {boolean} */ 56 | let escape; 57 | /** @type {boolean} */ 58 | let escaped; 59 | /** @type {number} */ 60 | let escapePos; 61 | /** @type {string} */ 62 | let prev; 63 | /** @type {number} */ 64 | let n; 65 | /** @type {Token} */ 66 | let currentToken; 67 | 68 | let length = css.length; 69 | let pos = 0; 70 | /** @type {Token[]} */ 71 | let buffer = []; 72 | /** @type {Token[]} */ 73 | let returned = []; 74 | 75 | function position() { 76 | return pos; 77 | } 78 | 79 | /** 80 | * @param {string} what 81 | */ 82 | function unclosed(what) { 83 | throw input.error('Unclosed ' + what, pos); 84 | } 85 | 86 | function endOfFile() { 87 | return returned.length === 0 && pos >= length; 88 | } 89 | 90 | /** 91 | * @param {{ ignoreUnclosed: any; }} [opts] 92 | */ 93 | function nextToken(opts) { 94 | if (returned.length) { 95 | return returned.pop(); 96 | } 97 | 98 | if (pos >= length) { 99 | return; // eslint-disable-line consistent-return 100 | } 101 | 102 | let ignoreUnclosed = opts ? opts.ignoreUnclosed : false; 103 | 104 | code = css.charCodeAt(pos); 105 | 106 | switch (code) { 107 | case NEWLINE: 108 | case SPACE: 109 | case TAB: 110 | case CR: 111 | case FEED: { 112 | next = pos; 113 | 114 | do { 115 | next += 1; 116 | code = css.charCodeAt(next); 117 | } while ( 118 | code === SPACE || 119 | code === NEWLINE || 120 | code === TAB || 121 | code === CR || 122 | code === FEED 123 | ); 124 | 125 | currentToken = ['space', css.slice(pos, next)]; 126 | pos = next - 1; 127 | break; 128 | } 129 | 130 | case OPEN_SQUARE: 131 | case CLOSE_SQUARE: 132 | case OPEN_CURLY: 133 | case CLOSE_CURLY: 134 | case COLON: 135 | case SEMICOLON: 136 | case CLOSE_PARENTHESES: { 137 | let controlChar = String.fromCharCode(code); 138 | 139 | currentToken = [controlChar, controlChar, pos]; 140 | break; 141 | } 142 | 143 | case OPEN_PARENTHESES: { 144 | prev = buffer.length > 0 ? /** @type {Token} */ (buffer.pop())[1] : ''; 145 | n = css.charCodeAt(pos + 1); 146 | 147 | if ( 148 | prev === 'url' && 149 | n !== SINGLE_QUOTE && 150 | n !== DOUBLE_QUOTE && 151 | n !== SPACE && 152 | n !== NEWLINE && 153 | n !== TAB && 154 | n !== FEED && 155 | n !== CR 156 | ) { 157 | next = pos; 158 | 159 | do { 160 | escaped = false; 161 | next = css.indexOf(')', next + 1); 162 | 163 | // STYLED PATCH { 164 | // Catch cases where interpolation inside url has brackets 165 | let interpolation = interpolations.find( 166 | (item) => item.start < next && next < item.end, 167 | ); 168 | 169 | if (interpolation) { 170 | next = css.indexOf(')', interpolation.end); 171 | } 172 | // } STYLED PATCH 173 | 174 | if (next === -1) { 175 | if (ignore || ignoreUnclosed) { 176 | next = pos; 177 | break; 178 | } else { 179 | unclosed('bracket'); 180 | } 181 | } 182 | 183 | escapePos = next; 184 | 185 | while (css.charCodeAt(escapePos - 1) === BACKSLASH) { 186 | escapePos -= 1; 187 | escaped = !escaped; 188 | } 189 | } while (escaped); 190 | 191 | currentToken = ['brackets', css.slice(pos, next + 1), pos, next]; 192 | 193 | pos = next; 194 | } else { 195 | next = css.indexOf(')', pos + 1); 196 | content = css.slice(pos, next + 1); 197 | 198 | if (next === -1 || RE_BAD_BRACKET.test(content)) { 199 | currentToken = ['(', '(', pos]; 200 | } else { 201 | currentToken = ['brackets', content, pos, next]; 202 | pos = next; 203 | } 204 | } 205 | 206 | break; 207 | } 208 | 209 | case SINGLE_QUOTE: 210 | case DOUBLE_QUOTE: { 211 | quote = code === SINGLE_QUOTE ? "'" : '"'; 212 | next = pos; 213 | 214 | do { 215 | escaped = false; 216 | next = css.indexOf(quote, next + 1); 217 | 218 | if (next === -1) { 219 | if (ignore || ignoreUnclosed) { 220 | next = pos + 1; 221 | break; 222 | } else { 223 | unclosed('string'); 224 | } 225 | } 226 | 227 | escapePos = next; 228 | 229 | while (css.charCodeAt(escapePos - 1) === BACKSLASH) { 230 | escapePos -= 1; 231 | escaped = !escaped; 232 | } 233 | } while (escaped); 234 | 235 | currentToken = ['string', css.slice(pos, next + 1), pos, next]; 236 | pos = next; 237 | break; 238 | } 239 | 240 | case AT: { 241 | RE_AT_END.lastIndex = pos + 1; 242 | RE_AT_END.test(css); 243 | 244 | if (RE_AT_END.lastIndex === 0) { 245 | next = css.length - 1; 246 | } else { 247 | next = RE_AT_END.lastIndex - 2; 248 | } 249 | 250 | currentToken = ['at-word', css.slice(pos, next + 1), pos, next]; 251 | 252 | pos = next; 253 | break; 254 | } 255 | 256 | case BACKSLASH: { 257 | next = pos; 258 | escape = true; 259 | 260 | while (css.charCodeAt(next + 1) === BACKSLASH) { 261 | next += 1; 262 | escape = !escape; 263 | } 264 | 265 | code = css.charCodeAt(next + 1); 266 | 267 | if ( 268 | escape && 269 | code !== SLASH && 270 | code !== SPACE && 271 | code !== NEWLINE && 272 | code !== TAB && 273 | code !== CR && 274 | code !== FEED 275 | ) { 276 | next += 1; 277 | 278 | if (RE_HEX_ESCAPE.test(css.charAt(next))) { 279 | while (RE_HEX_ESCAPE.test(css.charAt(next + 1))) { 280 | next += 1; 281 | } 282 | 283 | if (css.charCodeAt(next + 1) === SPACE) { 284 | next += 1; 285 | } 286 | } 287 | } 288 | 289 | currentToken = ['word', css.slice(pos, next + 1), pos, next]; 290 | 291 | pos = next; 292 | break; 293 | } 294 | 295 | default: { 296 | // STYLED PATCH { 297 | if (code === DOLLAR_SIGN) { 298 | let interpolation = interpolations.find((item) => item.start === pos); 299 | 300 | if (interpolation) { 301 | next = interpolation.end; 302 | currentToken = ['word', css.slice(pos, next + 1), pos, next]; 303 | buffer.push(currentToken); 304 | pos = next; 305 | } 306 | // } STYLED PATCH 307 | } else if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) { 308 | next = css.indexOf('*/', pos + 2) + 1; 309 | 310 | if (next === 0) { 311 | if (ignore || ignoreUnclosed) { 312 | next = css.length; 313 | } else { 314 | unclosed('comment'); 315 | } 316 | } 317 | 318 | currentToken = ['comment', css.slice(pos, next + 1), pos, next]; 319 | pos = next; 320 | } else { 321 | RE_WORD_END.lastIndex = pos + 1; 322 | RE_WORD_END.test(css); 323 | 324 | if (RE_WORD_END.lastIndex === 0) { 325 | next = css.length - 1; 326 | } else { 327 | next = RE_WORD_END.lastIndex - 2; 328 | } 329 | 330 | // STYLED PATCH { 331 | let interpolation = interpolations.find( 332 | (item) => pos <= item.start && item.start <= next + 1, 333 | ); 334 | 335 | // Catching things like `.${css}`, where symbol is immediatelly followed by interpolation 336 | if (interpolation) { 337 | next = interpolation.end; 338 | } 339 | // } STYLED PATCH 340 | 341 | currentToken = ['word', css.slice(pos, next + 1), pos, next]; 342 | buffer.push(currentToken); 343 | pos = next; 344 | } 345 | 346 | break; 347 | } 348 | } 349 | 350 | pos++; 351 | 352 | return currentToken; 353 | } 354 | 355 | /** 356 | * @param {Token} token 357 | */ 358 | function back(token) { 359 | returned.push(token); 360 | } 361 | 362 | return { 363 | back, 364 | nextToken, 365 | endOfFile, 366 | position, 367 | }; 368 | } 369 | 370 | module.exports = tokenizer; 371 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 'use strict'; 3 | 4 | let { Declaration, Rule } = require('postcss'); 5 | let PostCSSParser = require('postcss/lib/parser'); 6 | let tokenizer = require('./tokenizer'); 7 | 8 | // eslint-disable-next-line consistent-return 9 | function findLastWithPosition(tokens) { 10 | for (let i = tokens.length - 1; i >= 0; i--) { 11 | let token = tokens[i]; 12 | let pos = token[3] || token[2]; 13 | 14 | if (pos) return pos; 15 | } 16 | } 17 | 18 | class Parser extends PostCSSParser { 19 | constructor(input, options) { 20 | super(input); 21 | this.interpolations = options.interpolations; 22 | 23 | this.createTokenizer(options); 24 | this.root.source = { 25 | input, 26 | start: { 27 | offset: options.raws.rangeStart, 28 | line: options.raws.locationStart.line, 29 | column: options.raws.locationStart.column, 30 | }, 31 | }; 32 | 33 | this.root.raws.styledSyntaxRangeStart = options.raws.rangeStart; 34 | this.root.raws.styledSyntaxRangeEnd = options.raws.rangeEnd; 35 | 36 | // Add a flag to enable PostCSS and Stylelint to adjust to CSS-in-JS quirks 37 | // E. g. if something processes only rules, it could also process `root` if this flag is present 38 | this.root.raws.isRuleLike = true; 39 | 40 | if (options.isComponent) { 41 | this.root.raws.styledSyntaxIsComponent = true; 42 | } else { 43 | this.root.raws.styledOriginalContent = options.raws.originalContent; 44 | } 45 | } 46 | 47 | createTokenizer(tokenizerOptions) { 48 | this.tokenizer = tokenizer(this.input, tokenizerOptions); 49 | } 50 | 51 | parse() { 52 | // STYLED PATCH The only difference from PostCSS parser is try-catch wrap 53 | // Catching errors here to catch errors from both parser and tokenizer and fix reported error positions 54 | try { 55 | let token; 56 | 57 | while (!this.tokenizer.endOfFile()) { 58 | token = this.tokenizer.nextToken(); 59 | 60 | switch (token[0]) { 61 | case 'space': { 62 | this.spaces += token[1]; 63 | break; 64 | } 65 | 66 | case ';': { 67 | this.freeSemicolon(token); 68 | break; 69 | } 70 | 71 | case '}': { 72 | this.end(token); 73 | break; 74 | } 75 | 76 | case 'comment': { 77 | this.comment(token); 78 | break; 79 | } 80 | 81 | case 'at-word': { 82 | this.atrule(token); 83 | break; 84 | } 85 | 86 | case '{': { 87 | this.emptyRule(token); 88 | break; 89 | } 90 | 91 | default: { 92 | this.other(token); 93 | break; 94 | } 95 | } 96 | } 97 | 98 | this.endFile(); 99 | } catch (error) { 100 | throw this.fixErrorPosition(error); 101 | } 102 | } 103 | 104 | other(start) { 105 | let end = false; 106 | let type = null; 107 | let colon = false; 108 | let bracket = null; 109 | let brackets = []; 110 | let customProperty = start[1].startsWith('--'); 111 | 112 | let tokens = []; 113 | let token = start; 114 | 115 | while (token) { 116 | type = token[0]; 117 | tokens.push(token); 118 | 119 | if (type === '(' || type === '[') { 120 | if (!bracket) bracket = token; 121 | 122 | brackets.push(type === '(' ? ')' : ']'); 123 | } else if (customProperty && colon && type === '{') { 124 | if (!bracket) bracket = token; 125 | 126 | brackets.push('}'); 127 | } else if (brackets.length === 0) { 128 | if (type === ';') { 129 | if (colon) { 130 | this.decl(tokens, customProperty); 131 | 132 | return; 133 | } 134 | 135 | break; 136 | } else if (type === '{') { 137 | this.rule(tokens); 138 | 139 | return; 140 | } else if (type === '}') { 141 | this.tokenizer.back(tokens.pop()); 142 | end = true; 143 | break; 144 | } else if (type === ':') { 145 | colon = true; 146 | // STYLED PATCH { 147 | } else if (type === 'at-word') { 148 | this.tokenizer.back(tokens.pop()); 149 | end = true; 150 | break; 151 | // } STYLED PATCH 152 | } 153 | } else if (type === brackets[brackets.length - 1]) { 154 | brackets.pop(); 155 | 156 | if (brackets.length === 0) bracket = null; 157 | } 158 | 159 | token = this.tokenizer.nextToken(); 160 | } 161 | 162 | if (this.tokenizer.endOfFile()) end = true; 163 | 164 | if (brackets.length > 0) this.unclosedBracket(bracket); 165 | 166 | if (end && colon) { 167 | if (!customProperty) { 168 | while (tokens.length) { 169 | token = tokens[tokens.length - 1][0]; 170 | 171 | if (token !== 'space' && token !== 'comment') break; 172 | 173 | this.tokenizer.back(tokens.pop()); 174 | } 175 | } 176 | 177 | this.decl(tokens, customProperty); 178 | 179 | // STYLED PATCH { 180 | return; 181 | // } STYLED PATCH 182 | } 183 | 184 | // STYLED PATCH { 185 | for (token of tokens) { 186 | if (this.isInterpolation(token) || token[0] === 'space' || token[0] === ';') { 187 | this.spaces += token[1]; 188 | } else if (token[0] === 'comment') { 189 | this.comment(token); 190 | } else { 191 | this.unknownWord([token]); 192 | } 193 | } 194 | // } STYLED PATCH 195 | } 196 | 197 | rule(tokens) { 198 | // Removes { 199 | tokens.pop(); 200 | 201 | // STYLED PATCH { 202 | this.spaces += this.spacesAndInterpolationsFromStart(tokens); 203 | // } STYLED PATCH 204 | 205 | let node = new Rule(); 206 | 207 | this.init(node, tokens[0][2]); 208 | 209 | node.raws.between = this.spacesAndCommentsFromEnd(tokens); 210 | this.raw(node, 'selector', tokens); 211 | this.current = node; 212 | } 213 | 214 | decl(tokens, customProperty) { 215 | let node = new Declaration(); 216 | 217 | this.init(node, tokens[0][2]); 218 | 219 | let last = tokens[tokens.length - 1]; 220 | 221 | if (last[0] === ';') { 222 | this.semicolon = true; 223 | tokens.pop(); 224 | } 225 | 226 | node.source.end = this.getPosition(last[3] || last[2] || findLastWithPosition(tokens)); 227 | 228 | // STYLED PATCH { 229 | // Add all “lose” words to node raws 230 | while ( 231 | tokens[0][0] !== 'word' || 232 | (this.isInterpolation(tokens[0]) && tokens[1][0] === 'space') || 233 | (this.isInterpolation(tokens[0]) && this.isInterpolation(tokens[1])) 234 | ) { 235 | // } STYLED PATCH 236 | if (tokens.length === 1) this.unknownWord(tokens); 237 | 238 | node.raws.before += tokens.shift()[1]; 239 | } 240 | 241 | node.source.start = this.getPosition(tokens[0][2]); 242 | 243 | node.prop = ''; 244 | 245 | while (tokens.length) { 246 | let type = tokens[0][0]; 247 | 248 | if (type === ':' || type === 'space' || type === 'comment') { 249 | break; 250 | } 251 | 252 | node.prop += tokens.shift()[1]; 253 | } 254 | 255 | node.raws.between = ''; 256 | 257 | let token; 258 | 259 | while (tokens.length) { 260 | token = tokens.shift(); 261 | 262 | if (token[0] === ':') { 263 | node.raws.between += token[1]; 264 | break; 265 | } else { 266 | if (token[0] === 'word' && /\w/.test(token[1])) { 267 | this.unknownWord([token]); 268 | } 269 | 270 | node.raws.between += token[1]; 271 | } 272 | } 273 | 274 | if (node.prop[0] === '_' || node.prop[0] === '*') { 275 | node.raws.before += node.prop[0]; 276 | node.prop = node.prop.slice(1); 277 | } 278 | 279 | let firstSpaces = []; 280 | let next; 281 | 282 | while (tokens.length) { 283 | next = tokens[0][0]; 284 | 285 | if (next !== 'space' && next !== 'comment') break; 286 | 287 | firstSpaces.push(tokens.shift()); 288 | } 289 | 290 | this.precheckMissedSemicolon(tokens); 291 | 292 | for (let i = tokens.length - 1; i >= 0; i--) { 293 | token = tokens[i]; 294 | 295 | if (token[1].toLowerCase() === '!important') { 296 | node.important = true; 297 | let string = this.stringFrom(tokens, i); 298 | 299 | string = this.spacesFromEnd(tokens) + string; 300 | 301 | if (string !== ' !important') node.raws.important = string; 302 | 303 | break; 304 | } else if (token[1].toLowerCase() === 'important') { 305 | let cache = [...tokens]; 306 | let str = ''; 307 | 308 | for (let j = i; j > 0; j--) { 309 | let type = cache[j][0]; 310 | 311 | if (str.trim().indexOf('!') === 0 && type !== 'space') { 312 | break; 313 | } 314 | 315 | str = cache.pop()[1] + str; 316 | } 317 | 318 | if (str.trim().indexOf('!') === 0) { 319 | node.important = true; 320 | node.raws.important = str; 321 | tokens = cache; // eslint-disable-line no-param-reassign 322 | } 323 | } 324 | 325 | if (token[0] !== 'space' && token[0] !== 'comment') { 326 | break; 327 | } 328 | } 329 | 330 | let hasWord = tokens.some((i) => i[0] !== 'space' && i[0] !== 'comment'); 331 | 332 | if (hasWord) { 333 | node.raws.between += firstSpaces.map((i) => i[1]).join(''); 334 | firstSpaces = []; 335 | } 336 | 337 | this.raw(node, 'value', firstSpaces.concat(tokens), customProperty); // eslint-disable-line unicorn/prefer-spread 338 | 339 | if (node.value.includes(':') && !customProperty) { 340 | this.checkMissedSemicolon(tokens); 341 | } 342 | } 343 | 344 | // Helpers 345 | 346 | fixLine(line) { 347 | return this.root.source.start.line - 1 + line; 348 | } 349 | 350 | fixColumn(line, column) { 351 | let isSameLineAsRootStart = line === 1; 352 | 353 | return isSameLineAsRootStart ? this.root.source.start.column - 1 + column : column; 354 | } 355 | 356 | unfixPosition(position) { 357 | return { 358 | offset: position.offset - this.root.source.start.offset, 359 | line: position.line - this.root.source.start.line + 1, 360 | column: 361 | position.line === this.root.source.start.line 362 | ? position.column - this.root.source.start.column + 1 363 | : position.column, 364 | }; 365 | } 366 | 367 | getPosition(offset) { 368 | let pos = this.input.fromOffset(offset); 369 | 370 | // STYLED PATCH { 371 | return { 372 | offset: this.root.source.start.offset + offset, 373 | line: this.fixLine(pos.line), 374 | column: this.fixColumn(pos.line, pos.col), 375 | }; 376 | // } STYLED PATCH 377 | } 378 | 379 | isInterpolation(token) { 380 | if (token[0] !== 'word') { 381 | return false; 382 | } 383 | 384 | return this.interpolations?.some( 385 | (item) => item.start === token[2] && item.end === token[3], 386 | ); 387 | } 388 | 389 | spacesAndInterpolationsFromStart(tokens) { 390 | let spaces = ''; 391 | let removedTokens = []; 392 | let hasInterpolation = false; 393 | let tokensLength = tokens.length; 394 | let index; 395 | 396 | for (index = 0; index < tokensLength; index++) { 397 | let current = tokens.shift(); 398 | 399 | spaces += current[1]; 400 | removedTokens.push(current); 401 | 402 | if (index === 0 && this.isInterpolation(current)) { 403 | hasInterpolation = true; 404 | } 405 | 406 | if (current[0] === 'space' && current[1].includes('\n')) { 407 | const nextToken = tokens[0]; 408 | 409 | if (nextToken && this.isInterpolation(nextToken)) { 410 | continue; 411 | } 412 | 413 | break; 414 | } 415 | } 416 | 417 | // If all tokens were cycled through, then it means there were no interpolation on a separate line 418 | if (index === tokensLength || !hasInterpolation) { 419 | tokens.unshift(...removedTokens); 420 | 421 | return ''; 422 | } 423 | 424 | return spaces; 425 | } 426 | 427 | // Errors 428 | 429 | fixErrorPosition(rawError) { 430 | let error = rawError; 431 | 432 | if (error.name === 'CssSyntaxError') { 433 | if (error.line) { 434 | error.line = this.fixLine(error.line); 435 | 436 | if (error.column) { 437 | error.column = this.fixColumn(error.line, error.column); 438 | } 439 | } 440 | 441 | if (error.endLine) { 442 | error.endLine = this.fixLine(error.endLine); 443 | 444 | if (error.endColumn) { 445 | error.endColumn = this.fixColumn(error.endLine, error.endColumn); 446 | } 447 | } 448 | 449 | if (error.input) { 450 | if (error.input.line) { 451 | error.input.line = this.fixLine(error.input.line); 452 | 453 | if (error.input.column) { 454 | error.input.column = this.fixColumn(error.input.line, error.input.column); 455 | } 456 | } 457 | 458 | if (error.input.endLine) { 459 | error.input.endLine = this.fixLine(error.input.endLine); 460 | 461 | if (error.input.endColumn) { 462 | error.input.endColumn = this.fixColumn( 463 | error.input.endLine, 464 | error.input.endColumn, 465 | ); 466 | } 467 | } 468 | } 469 | 470 | error.message = error.message.replace(/:\d+:\d+:/, `:${error.line}:${error.column}:`); 471 | } 472 | 473 | throw error; 474 | } 475 | 476 | unclosedBlock() { 477 | // STYLED PATCH { 478 | let pos = this.unfixPosition(this.current.source.start); 479 | // } STYLED PATCH 480 | 481 | throw this.input.error('Unclosed block', pos.line, pos.column); 482 | } 483 | } 484 | 485 | module.exports = Parser; 486 | -------------------------------------------------------------------------------- /lib/__tests__/tokenizer.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-concat */ 2 | let tokenizer = require('../tokenizer'); 3 | let { Input } = require('postcss'); 4 | 5 | /** 6 | * @param {string} css 7 | * @param {import('../tokenizer').TokenizerOptions} [opts] 8 | */ 9 | function tokenize(css, opts) { 10 | let processor = tokenizer(new Input(css), opts); 11 | let tokens = []; 12 | 13 | while (!processor.endOfFile()) { 14 | tokens.push(processor.nextToken()); 15 | } 16 | 17 | return tokens; 18 | } 19 | 20 | /** 21 | * @param {string} css 22 | * @param {Array>} tokens 23 | * @param {tokenizer.TokenizerOptions} [opts] 24 | */ 25 | function run(css, tokens, opts) { 26 | expect(tokenize(css, opts)).toEqual(tokens); 27 | } 28 | 29 | describe('interpolations', () => { 30 | test('declaration value', () => { 31 | run( 32 | 'color: ${va};', 33 | [ 34 | ['word', 'color', 0, 4], 35 | [':', ':', 5], 36 | ['space', ' '], 37 | ['word', '${va}', 7, 11], 38 | [';', ';', 12], 39 | ], 40 | { interpolations: [{ start: 7, end: 11 }] }, 41 | ); 42 | }); 43 | 44 | test('declaration value with url function and simple interpolation', () => { 45 | run( 46 | 'background-image: url(${props.backgroundImageFdfsdfsdfsd})', 47 | [ 48 | ['word', 'background-image', 0, 15], 49 | [':', ':', 16], 50 | ['space', ' '], 51 | ['word', 'url', 18, 20], 52 | ['brackets', '(${props.backgroundImageFdfsdfsdfsd})', 21, 57], 53 | ], 54 | { interpolations: [{ start: 22, end: 56 }] }, 55 | ); 56 | }); 57 | 58 | test('declaration value with url function and brackets inside interpolation', () => { 59 | run( 60 | 'background-image: url(${(props) => props.backgroundImage})', 61 | [ 62 | ['word', 'background-image', 0, 15], 63 | [':', ':', 16], 64 | ['space', ' '], 65 | ['word', 'url', 18, 20], 66 | ['brackets', '(${(props) => props.backgroundImage})', 21, 57], 67 | ], 68 | { interpolations: [{ start: 22, end: 56 }] }, 69 | ); 70 | }); 71 | 72 | test('declaration property', () => { 73 | run( 74 | '${co}: black;', 75 | [ 76 | ['word', '${co}', 0, 4], 77 | [':', ':', 5], 78 | ['space', ' '], 79 | ['word', 'black', 7, 11], 80 | [';', ';', 12], 81 | ], 82 | { interpolations: [{ start: 0, end: 4 }] }, 83 | ); 84 | }); 85 | 86 | test('standalone before rule without semicolon', () => { 87 | let css = '${props}\n' + '\n' + 'a {}'; 88 | 89 | run( 90 | css, 91 | [ 92 | ['word', '${props}', 0, 7], 93 | ['space', '\n\n'], 94 | ['word', 'a', 10, 10], 95 | ['space', ' '], 96 | ['{', '{', 12], 97 | ['}', '}', 13], 98 | ], 99 | { interpolations: [{ start: 0, end: 7 }] }, 100 | ); 101 | }); 102 | 103 | test('standalone before rule with semicolon', () => { 104 | let css = '${props};\n' + '\n' + 'a {}'; 105 | 106 | run( 107 | css, 108 | [ 109 | ['word', '${props}', 0, 7], 110 | [';', ';', 8], 111 | ['space', '\n\n'], 112 | ['word', 'a', 11, 11], 113 | ['space', ' '], 114 | ['{', '{', 13], 115 | ['}', '}', 14], 116 | ], 117 | { interpolations: [{ start: 0, end: 7 }] }, 118 | ); 119 | }); 120 | 121 | test('standalone after rule without semicolon', () => { 122 | run( 123 | 'a {}\n\n${props}', 124 | [ 125 | ['word', 'a', 0, 0], 126 | ['space', ' '], 127 | ['{', '{', 2], 128 | ['}', '}', 3], 129 | ['space', '\n\n'], 130 | ['word', '${props}', 6, 13], 131 | ], 132 | { interpolations: [{ start: 6, end: 13 }] }, 133 | ); 134 | }); 135 | 136 | test('standalone before rule with semicolon', () => { 137 | run( 138 | 'a {}\n\n${props};', 139 | [ 140 | ['word', 'a', 0, 0], 141 | ['space', ' '], 142 | ['{', '{', 2], 143 | ['}', '}', 3], 144 | ['space', '\n\n'], 145 | ['word', '${props}', 6, 13], 146 | [';', ';', 14], 147 | ], 148 | { interpolations: [{ start: 6, end: 13 }] }, 149 | ); 150 | }); 151 | 152 | test('selector with interpolation', () => { 153 | run( 154 | '${cls} a {}', 155 | [ 156 | ['word', '${cls}', 0, 5], 157 | ['space', ' '], 158 | ['word', 'a', 7, 7], 159 | ['space', ' '], 160 | ['{', '{', 9], 161 | ['}', '}', 10], 162 | ], 163 | { interpolations: [{ start: 0, end: 5 }] }, 164 | ); 165 | }); 166 | 167 | test('selector with interpolation, which has symbot immedittelly before', () => { 168 | run( 169 | '.${cls} a {}', 170 | [ 171 | ['word', '.${cls}', 0, 6], 172 | ['space', ' '], 173 | ['word', 'a', 8, 8], 174 | ['space', ' '], 175 | ['{', '{', 10], 176 | ['}', '}', 11], 177 | ], 178 | { interpolations: [{ start: 1, end: 6 }] }, 179 | ); 180 | }); 181 | 182 | test('interpolation with semicolon before at-rule', () => { 183 | run( 184 | '${borderWidth};@media screen {}', 185 | [ 186 | ['word', '${borderWidth}', 0, 13], 187 | [';', ';', 14], 188 | ['at-word', '@media', 15, 20], 189 | ['space', ' '], 190 | ['word', 'screen', 22, 27], 191 | ['space', ' '], 192 | ['{', '{', 29], 193 | ['}', '}', 30], 194 | ], 195 | { interpolations: [{ start: 0, end: 13 }] }, 196 | ); 197 | }); 198 | 199 | test('interpolation with semicolon before at-rule', () => { 200 | run( 201 | '${borderWidth} @media screen {}', 202 | [ 203 | ['word', '${borderWidth}', 0, 13], 204 | ['space', ' '], 205 | ['at-word', '@media', 15, 20], 206 | ['space', ' '], 207 | ['word', 'screen', 22, 27], 208 | ['space', ' '], 209 | ['{', '{', 29], 210 | ['}', '}', 30], 211 | ], 212 | { interpolations: [{ start: 0, end: 13 }] }, 213 | ); 214 | }); 215 | }); 216 | 217 | describe('standard PostCSS tests', () => { 218 | test('tokenizes empty file', () => { 219 | run('', []); 220 | }); 221 | 222 | test('tokenizes space', () => { 223 | run('\r\n \f\t', [['space', '\r\n \f\t']]); 224 | }); 225 | 226 | test('tokenizes word', () => { 227 | run('ab', [['word', 'ab', 0, 1]]); 228 | }); 229 | 230 | test('splits word by !', () => { 231 | run('aa!bb', [ 232 | ['word', 'aa', 0, 1], 233 | ['word', '!bb', 2, 4], 234 | ]); 235 | }); 236 | 237 | test('changes lines in spaces', () => { 238 | run('a \n b', [ 239 | ['word', 'a', 0, 0], 240 | ['space', ' \n '], 241 | ['word', 'b', 4, 4], 242 | ]); 243 | }); 244 | 245 | test('tokenizes control chars', () => { 246 | run('{:;}', [ 247 | ['{', '{', 0], 248 | [':', ':', 1], 249 | [';', ';', 2], 250 | ['}', '}', 3], 251 | ]); 252 | }); 253 | 254 | test('escapes control symbols', () => { 255 | run('\\(\\{\\"\\@\\\\""', [ 256 | ['word', '\\(', 0, 1], 257 | ['word', '\\{', 2, 3], 258 | ['word', '\\"', 4, 5], 259 | ['word', '\\@', 6, 7], 260 | ['word', '\\\\', 8, 9], 261 | ['string', '""', 10, 11], 262 | ]); 263 | }); 264 | 265 | test('escapes backslash', () => { 266 | run('\\\\\\\\{', [ 267 | ['word', '\\\\\\\\', 0, 3], 268 | ['{', '{', 4], 269 | ]); 270 | }); 271 | 272 | test('tokenizes simple brackets', () => { 273 | run('(ab)', [['brackets', '(ab)', 0, 3]]); 274 | }); 275 | 276 | test('tokenizes square brackets', () => { 277 | run('a[bc]', [ 278 | ['word', 'a', 0, 0], 279 | ['[', '[', 1], 280 | ['word', 'bc', 2, 3], 281 | [']', ']', 4], 282 | ]); 283 | }); 284 | 285 | test('tokenizes complicated brackets', () => { 286 | run('(())("")(/**/)(\\\\)(\n)(', [ 287 | ['(', '(', 0], 288 | ['brackets', '()', 1, 2], 289 | [')', ')', 3], 290 | ['(', '(', 4], 291 | ['string', '""', 5, 6], 292 | [')', ')', 7], 293 | ['(', '(', 8], 294 | ['comment', '/**/', 9, 12], 295 | [')', ')', 13], 296 | ['(', '(', 14], 297 | ['word', '\\\\', 15, 16], 298 | [')', ')', 17], 299 | ['(', '(', 18], 300 | ['space', '\n'], 301 | [')', ')', 20], 302 | ['(', '(', 21], 303 | ]); 304 | }); 305 | 306 | test('tokenizes string', () => { 307 | run('\'"\'"\\""', [ 308 | ['string', "'\"'", 0, 2], 309 | ['string', '"\\""', 3, 6], 310 | ]); 311 | }); 312 | 313 | test('tokenizes escaped string', () => { 314 | run('"\\\\"', [['string', '"\\\\"', 0, 3]]); 315 | }); 316 | 317 | test('changes lines in strings', () => { 318 | run('"\n\n""\n\n"', [ 319 | ['string', '"\n\n"', 0, 3], 320 | ['string', '"\n\n"', 4, 7], 321 | ]); 322 | }); 323 | 324 | test('tokenizes at-word', () => { 325 | run('@word ', [ 326 | ['at-word', '@word', 0, 4], 327 | ['space', ' '], 328 | ]); 329 | }); 330 | 331 | test('tokenizes at-word end', () => { 332 | run('@one{@two()@three""@four;', [ 333 | ['at-word', '@one', 0, 3], 334 | ['{', '{', 4], 335 | ['at-word', '@two', 5, 8], 336 | ['brackets', '()', 9, 10], 337 | ['at-word', '@three', 11, 16], 338 | ['string', '""', 17, 18], 339 | ['at-word', '@four', 19, 23], 340 | [';', ';', 24], 341 | ]); 342 | }); 343 | 344 | test('tokenizes urls', () => { 345 | run('url(/*\\))', [ 346 | ['word', 'url', 0, 2], 347 | ['brackets', '(/*\\))', 3, 8], 348 | ]); 349 | }); 350 | 351 | test('tokenizes quoted urls', () => { 352 | run('url(")")', [ 353 | ['word', 'url', 0, 2], 354 | ['(', '(', 3], 355 | ['string', '")"', 4, 6], 356 | [')', ')', 7], 357 | ]); 358 | }); 359 | 360 | test('tokenizes at-symbol', () => { 361 | run('@', [['at-word', '@', 0, 0]]); 362 | }); 363 | 364 | test('tokenizes comment', () => { 365 | run('/* a\nb */', [['comment', '/* a\nb */', 0, 8]]); 366 | }); 367 | 368 | test('changes lines in comments', () => { 369 | run('a/* \n */b', [ 370 | ['word', 'a', 0, 0], 371 | ['comment', '/* \n */', 1, 7], 372 | ['word', 'b', 8, 8], 373 | ]); 374 | }); 375 | 376 | test('supports line feed', () => { 377 | run('a\fb', [ 378 | ['word', 'a', 0, 0], 379 | ['space', '\f'], 380 | ['word', 'b', 2, 2], 381 | ]); 382 | }); 383 | 384 | test('supports carriage return', () => { 385 | run('a\rb\r\nc', [ 386 | ['word', 'a', 0, 0], 387 | ['space', '\r'], 388 | ['word', 'b', 2, 2], 389 | ['space', '\r\n'], 390 | ['word', 'c', 5, 5], 391 | ]); 392 | }); 393 | 394 | test('tokenizes CSS', () => { 395 | let css = 396 | 'a {\n' + 397 | ' content: "a";\n' + 398 | ' width: calc(1px;)\n' + 399 | ' }\n' + 400 | '/* small screen */\n' + 401 | '@media screen {}'; 402 | 403 | run(css, [ 404 | ['word', 'a', 0, 0], 405 | ['space', ' '], 406 | ['{', '{', 2], 407 | ['space', '\n '], 408 | ['word', 'content', 6, 12], 409 | [':', ':', 13], 410 | ['space', ' '], 411 | ['string', '"a"', 15, 17], 412 | [';', ';', 18], 413 | ['space', '\n '], 414 | ['word', 'width', 22, 26], 415 | [':', ':', 27], 416 | ['space', ' '], 417 | ['word', 'calc', 29, 32], 418 | ['brackets', '(1px;)', 33, 38], 419 | ['space', '\n '], 420 | ['}', '}', 42], 421 | ['space', '\n'], 422 | ['comment', '/* small screen */', 44, 61], 423 | ['space', '\n'], 424 | ['at-word', '@media', 63, 68], 425 | ['space', ' '], 426 | ['word', 'screen', 70, 75], 427 | ['space', ' '], 428 | ['{', '{', 77], 429 | ['}', '}', 78], 430 | ]); 431 | }); 432 | 433 | test('throws error on unclosed string', () => { 434 | expect(() => { 435 | tokenize(' "'); 436 | }).toThrow(/:1:2: Unclosed string/); 437 | }); 438 | 439 | test('throws error on unclosed comment', () => { 440 | expect(() => { 441 | tokenize(' /*'); 442 | }).toThrow(/:1:2: Unclosed comment/); 443 | }); 444 | 445 | test('throws error on unclosed url', () => { 446 | expect(() => { 447 | tokenize('url('); 448 | }).toThrow(/:1:4: Unclosed bracket/); 449 | }); 450 | 451 | test('ignores unclosing string on request', () => { 452 | run( 453 | ' "', 454 | [ 455 | ['space', ' '], 456 | ['string', '"', 1, 2], 457 | ], 458 | { ignoreErrors: true }, 459 | ); 460 | }); 461 | 462 | test('ignores unclosing comment on request', () => { 463 | run( 464 | ' /*', 465 | [ 466 | ['space', ' '], 467 | ['comment', '/*', 1, 3], 468 | ], 469 | { ignoreErrors: true }, 470 | ); 471 | }); 472 | 473 | test('ignores unclosing function on request', () => { 474 | run( 475 | 'url(', 476 | [ 477 | ['word', 'url', 0, 2], 478 | ['brackets', '(', 3, 3], 479 | ], 480 | { ignoreErrors: true }, 481 | ); 482 | }); 483 | 484 | test('tokenizes hexadecimal escape', () => { 485 | run('\\0a \\09 \\z ', [ 486 | ['word', '\\0a ', 0, 3], 487 | ['word', '\\09 ', 4, 7], 488 | ['word', '\\z', 8, 9], 489 | ['space', ' '], 490 | ]); 491 | }); 492 | 493 | test('ignore unclosed per token request', () => { 494 | /** 495 | * @param {string} css 496 | * @param {import('../tokenizer').TokenizerOptions} opts 497 | */ 498 | function token(css, opts) { 499 | let processor = tokenizer(new Input(css), opts); 500 | let tokens = []; 501 | 502 | while (!processor.endOfFile()) { 503 | tokens.push(processor.nextToken({ ignoreUnclosed: true })); 504 | } 505 | 506 | return tokens; 507 | } 508 | 509 | let css = "How's it going ("; 510 | let tokens = token(css, {}); 511 | let expected = [ 512 | ['word', 'How', 0, 2], 513 | ['string', "'s", 3, 4], 514 | ['space', ' '], 515 | ['word', 'it', 6, 7], 516 | ['space', ' '], 517 | ['word', 'going', 9, 13], 518 | ['space', ' '], 519 | ['(', '(', 15], 520 | ]; 521 | 522 | expect(tokens).toEqual(expected); 523 | }); 524 | 525 | test('provides correct position', () => { 526 | let css = 'Three tokens'; 527 | let processor = tokenizer(new Input(css)); 528 | 529 | expect(processor.position()).toBe(0); 530 | processor.nextToken(); 531 | expect(processor.position()).toBe(5); 532 | processor.nextToken(); 533 | expect(processor.position()).toBe(6); 534 | processor.nextToken(); 535 | expect(processor.position()).toBe(12); 536 | processor.nextToken(); 537 | expect(processor.position()).toBe(12); 538 | }); 539 | }); 540 | -------------------------------------------------------------------------------- /lib/__tests__/parseJs.test.js: -------------------------------------------------------------------------------- 1 | let { parseJs } = require('../parseJs'); 2 | 3 | describe('no interpolations', () => { 4 | test('one component', () => { 5 | let document = parseJs('let Component = styled.div`color: red;`;'); 6 | 7 | expect(document).toMatchSnapshot(); 8 | }); 9 | 10 | test('two components', () => { 11 | let document = parseJs( 12 | 'let Component = styled.div`color: red;`;\nlet Component = styled.div`border-color: blue`;', 13 | ); 14 | 15 | expect(document).toMatchSnapshot(); 16 | }); 17 | 18 | test('empty component', () => { 19 | let document = parseJs('let Component = styled.div``;'); 20 | 21 | expect(document).toMatchSnapshot(); 22 | }); 23 | 24 | test('property on its row. no extra space', () => { 25 | let document = parseJs('let Component = styled.div`\n\tcolor: red;\n`;'); 26 | 27 | expect(document).toMatchSnapshot(); 28 | }); 29 | 30 | test('property on its row. extra space in the begining', () => { 31 | let document = parseJs('let Component = styled.div` \n\tcolor: red;\n`;'); 32 | 33 | expect(document).toMatchSnapshot(); 34 | }); 35 | 36 | test('empty file', () => { 37 | let document = parseJs(''); 38 | 39 | expect(document).toMatchSnapshot(); 40 | }); 41 | 42 | test('no components in a file', () => { 43 | let document = parseJs('function styled() { return false }'); 44 | 45 | expect(document).toMatchSnapshot(); 46 | }); 47 | 48 | test('selector could be on multiple lines', () => { 49 | let document = parseJs('let Component = styled.div`a,\n\tb { color: red; }`;'); 50 | 51 | expect(document).toMatchSnapshot(); 52 | }); 53 | 54 | test('comment in selector', () => { 55 | let document = parseJs('let Component = styled.div`a, /* hello */ b { color: red; }`;'); 56 | 57 | expect(document).toMatchSnapshot(); 58 | }); 59 | 60 | test('escaped characters in the code', () => { 61 | let document = parseJs('let Component = styled.div`content: "\\u200B";`;'); 62 | 63 | expect(document).toMatchSnapshot(); 64 | }); 65 | }); 66 | 67 | describe('simple interpolations', () => { 68 | describe('properties', () => { 69 | test('property value (no semicolon)', () => { 70 | let document = parseJs('let Component = styled.div`color: ${red}`;'); 71 | 72 | expect(document).toMatchSnapshot(); 73 | }); 74 | 75 | test('property value. interpolation with a comment after value', () => { 76 | let document = parseJs('let Component = styled.div`color: ${red /* hello */}`;'); 77 | 78 | expect(document).toMatchSnapshot(); 79 | }); 80 | 81 | test('property value. interpolation with a comment before value', () => { 82 | let document = parseJs('let Component = styled.div`color: ${/* hello */ red}`;'); 83 | 84 | expect(document).toMatchSnapshot(); 85 | }); 86 | 87 | test('property value with symbol right before interpolation', () => { 88 | let document = parseJs('let Component = styled.div`margin: -${space}`;'); 89 | 90 | expect(document).toMatchSnapshot(); 91 | }); 92 | 93 | test('property value (with semicolon)', () => { 94 | let document = parseJs('let Component = styled.div`color: ${red};`;'); 95 | 96 | expect(document).toMatchSnapshot(); 97 | }); 98 | 99 | test('property value and !important (no semicolon)', () => { 100 | let document = parseJs('let Component = styled.div`color: ${red} !important`;'); 101 | 102 | expect(document).toMatchSnapshot(); 103 | }); 104 | 105 | test('property value and !important (with semicolon)', () => { 106 | let document = parseJs('let Component = styled.div`color: ${red} !important;`;'); 107 | 108 | expect(document).toMatchSnapshot(); 109 | }); 110 | 111 | test('property value with two interpolations', () => { 112 | let document = parseJs( 113 | 'let Component = styled.div`box-shadow: ${elevation1}, ${elevation2}`;', 114 | ); 115 | 116 | expect(document).toMatchSnapshot(); 117 | }); 118 | 119 | test('property value with props interpolation', () => { 120 | let document = parseJs( 121 | 'let Component = styled.div`background-image: ${(props) => props.backgroundImage}`;', 122 | ); 123 | 124 | expect(document).toMatchSnapshot(); 125 | }); 126 | 127 | test('property value with interpolation inside url function', () => { 128 | let document = parseJs( 129 | 'let Component = styled.div`background-image: url(${imageUrl})`;', 130 | ); 131 | 132 | expect(document).toMatchSnapshot(); 133 | }); 134 | 135 | test('property value with props interpolation inside url function', () => { 136 | let document = parseJs( 137 | 'let Component = styled.div`background-image: url(${(props) => props.backgroundImage})`;', 138 | ); 139 | 140 | expect(document).toMatchSnapshot(); 141 | }); 142 | 143 | test('property name (first property)', () => { 144 | let document = parseJs('let Component = styled.div`${color}: red`;'); 145 | 146 | expect(document).toMatchSnapshot(); 147 | }); 148 | 149 | test('property name (second property)', () => { 150 | let document = parseJs('let Component = styled.div`display: flex; ${color}: red`;'); 151 | 152 | expect(document).toMatchSnapshot(); 153 | }); 154 | }); 155 | 156 | describe('selectors', () => { 157 | test('in selector (whole selector)', () => { 158 | let document = parseJs('let Component = styled.div`${Component} { color: red; }`;'); 159 | 160 | expect(document).toMatchSnapshot(); 161 | }); 162 | 163 | test('in selector (whole selector starts with a dot)', () => { 164 | let document = parseJs('let Component = styled.div`.${Component} { color: red; }`;'); 165 | 166 | expect(document).toMatchSnapshot(); 167 | }); 168 | 169 | test('in selector (first selector is interpolation)', () => { 170 | let document = parseJs('let Component = styled.div`${Component}, a { color: red; }`;'); 171 | 172 | expect(document).toMatchSnapshot(); 173 | }); 174 | 175 | test('in selector (second selector is interpolation, two selectors)', () => { 176 | let document = parseJs('let Component = styled.div`a, ${Component} { color: red; }`;'); 177 | 178 | expect(document).toMatchSnapshot(); 179 | }); 180 | 181 | test('in selector (second part is interpolation, one selector)', () => { 182 | let document = parseJs('let Component = styled.div`a ${Component} { color: red; }`;'); 183 | 184 | expect(document).toMatchSnapshot(); 185 | }); 186 | 187 | test('in selector (first part is interpolation, one selector)', () => { 188 | let document = parseJs('let Component = styled.div`${Component} a { color: red; }`;'); 189 | 190 | expect(document).toMatchSnapshot(); 191 | }); 192 | 193 | test('interpolation with semicolon before selector', () => { 194 | let document = parseJs('let Component = styled.div`${Component}; a { color: red; }`;'); 195 | 196 | expect(document).toMatchSnapshot(); 197 | }); 198 | 199 | test('interpolation on a new line before selector', () => { 200 | let document = parseJs('let Component = styled.div`${hello}\n\ta { color: red; }`;'); 201 | 202 | expect(document).toMatchSnapshot(); 203 | }); 204 | 205 | test('comment in selector with interpolation', () => { 206 | let document = parseJs( 207 | 'let Component = styled.div`${Card}:hover, /* hello */ b { color: red; }`;', 208 | ); 209 | 210 | expect(document).toMatchSnapshot(); 211 | }); 212 | }); 213 | 214 | describe('standalone interpolation', () => { 215 | test('after declaration (no semicolon, no space after)', () => { 216 | let document = parseJs('let Component = styled.div`color: red; ${borderWidth}`;'); 217 | 218 | expect(document).toMatchSnapshot(); 219 | }); 220 | 221 | test('after declaration (no semicolon, space after)', () => { 222 | let document = parseJs('let Component = styled.div`color: red; ${borderWidth} `;'); 223 | 224 | expect(document).toMatchSnapshot(); 225 | }); 226 | 227 | test('multiple after declaration (no semicolon, space after)', () => { 228 | let document = parseJs( 229 | 'let Component = styled.div`color: red; ${borderWidth} ${hello} `;', 230 | ); 231 | 232 | expect(document).toMatchSnapshot(); 233 | }); 234 | 235 | test('after declaration (with semicolon)', () => { 236 | let document = parseJs('let Component = styled.div`color: red; ${borderWidth};`;'); 237 | 238 | expect(document).toMatchSnapshot(); 239 | }); 240 | 241 | test('before declaration (no semicolon, no space)', () => { 242 | let document = parseJs('let Component = styled.div`${borderWidth}color: red`;'); 243 | 244 | expect(document).toMatchSnapshot(); 245 | }); 246 | 247 | test('before declaration (with space)', () => { 248 | let document = parseJs('let Component = styled.div`${borderWidth} color: red`;'); 249 | 250 | expect(document).toMatchSnapshot(); 251 | }); 252 | 253 | test('before declaration (with semicolon)', () => { 254 | let document = parseJs('let Component = styled.div`${borderWidth};color: red`;'); 255 | 256 | expect(document).toMatchSnapshot(); 257 | }); 258 | 259 | test('before declaration (with semicolon and space)', () => { 260 | let document = parseJs('let Component = styled.div`${borderWidth}; color: red`;'); 261 | 262 | expect(document).toMatchSnapshot(); 263 | }); 264 | 265 | test('between two declarations (no semicolon)', () => { 266 | let document = parseJs( 267 | 'let Component = styled.div`color: red; ${borderWidth} border-color: blue;`;', 268 | ); 269 | 270 | expect(document).toMatchSnapshot(); 271 | }); 272 | 273 | test('between two declarations (with semicolon)', () => { 274 | let document = parseJs( 275 | 'let Component = styled.div`color: red; ${borderWidth}; border-color: blue;`;', 276 | ); 277 | 278 | expect(document).toMatchSnapshot(); 279 | }); 280 | 281 | test('as the only content (no semicolon)', () => { 282 | let document = parseJs('let Component = styled.div`${color}`;'); 283 | 284 | expect(document).toMatchSnapshot(); 285 | }); 286 | 287 | test('as the only content (with semicolon)', () => { 288 | let document = parseJs('let Component = styled.div`${color};`;'); 289 | 290 | expect(document).toMatchSnapshot(); 291 | }); 292 | 293 | test('between three declarations (no semicolon)', () => { 294 | let document = parseJs( 295 | 'let Component = styled.div`color: red; ${borderWidth} border-color: blue; ${anotherThing} display: none;`;', 296 | ); 297 | 298 | expect(document).toMatchSnapshot(); 299 | }); 300 | 301 | test('between two declarations, and also trailing interpolation', () => { 302 | let document = parseJs( 303 | 'let Component = styled.div`color: red; ${ borderWidth} border-color: blue; ${anotherThing}`;', 304 | ); 305 | 306 | expect(document).toMatchSnapshot(); 307 | }); 308 | 309 | test('after comment (no semicolon)', () => { 310 | let document = parseJs('let Component = styled.div`/* hello */ ${borderWidth}`;'); 311 | 312 | expect(document).toMatchSnapshot(); 313 | }); 314 | 315 | test('after comment (with semicolon)', () => { 316 | let document = parseJs('let Component = styled.div`/* hello */ ${borderWidth};`;'); 317 | 318 | expect(document).toMatchSnapshot(); 319 | }); 320 | 321 | test('before comment (no semicolon, no space)', () => { 322 | let document = parseJs('let Component = styled.div`${borderWidth}/* hello */`;'); 323 | 324 | expect(document).toMatchSnapshot(); 325 | }); 326 | 327 | test('before comment (no semicolon, with space)', () => { 328 | let document = parseJs('let Component = styled.div`${borderWidth} /* hello */`;'); 329 | 330 | expect(document).toMatchSnapshot(); 331 | }); 332 | 333 | test('before comment (with semicolon, no space)', () => { 334 | let document = parseJs('let Component = styled.div`${borderWidth};/* hello */`;'); 335 | 336 | expect(document).toMatchSnapshot(); 337 | }); 338 | 339 | test('before comment (with semicolon and space)', () => { 340 | let document = parseJs('let Component = styled.div`${borderWidth}; /* hello */`;'); 341 | 342 | expect(document).toMatchSnapshot(); 343 | }); 344 | 345 | test('before comment. comment with a backslash', () => { 346 | let document = parseJs( 347 | 'let Component = styled.div`${borderWidth};/* comment with a backslash \\ */`;', 348 | ); 349 | 350 | expect(document).toMatchSnapshot(); 351 | }); 352 | 353 | test('before at-rule (with semicolon)', () => { 354 | let document = parseJs('let Component = styled.div`${borderWidth};@media screen {}`;'); 355 | 356 | expect(document).toMatchSnapshot(); 357 | }); 358 | 359 | test('before at-rule (without semicolon)', () => { 360 | let document = parseJs('let Component = styled.div`${borderWidth} @media screen {}`;'); 361 | 362 | expect(document).toMatchSnapshot(); 363 | }); 364 | }); 365 | 366 | describe('multiple standalone interpolations', () => { 367 | test('after declaration (no semicolon, no space after)', () => { 368 | let document = parseJs( 369 | 'let Component = styled.div`color: red; ${borderWidth}${borderWidth}`;', 370 | ); 371 | 372 | expect(document).toMatchSnapshot(); 373 | }); 374 | 375 | test('after declaration (no semicolon, space after)', () => { 376 | let document = parseJs( 377 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth} `;', 378 | ); 379 | 380 | expect(document).toMatchSnapshot(); 381 | }); 382 | 383 | test('after declaration (with semicolon)', () => { 384 | let document = parseJs( 385 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth};`;', 386 | ); 387 | 388 | expect(document).toMatchSnapshot(); 389 | }); 390 | 391 | test('before declaration (no semicolon, no space)', () => { 392 | let document = parseJs( 393 | 'let Component = styled.div`${borderWidth} ${background}color: red`;', 394 | ); 395 | 396 | expect(document).toMatchSnapshot(); 397 | }); 398 | 399 | test('before declaration (no semicolon, with space)', () => { 400 | let document = parseJs( 401 | 'let Component = styled.div`${borderWidth}${background} color: red`;', 402 | ); 403 | 404 | expect(document).toMatchSnapshot(); 405 | }); 406 | 407 | test('before declaration (no semicolon, with space, three interpolations)', () => { 408 | let document = parseJs( 409 | 'let Component = styled.div`${borderWidth}${background}${display} color: red`;', 410 | ); 411 | 412 | expect(document).toMatchSnapshot(); 413 | }); 414 | 415 | test('before declaration (with space)', () => { 416 | let document = parseJs( 417 | 'let Component = styled.div`${borderWidth} ${borderWidth} color: red`;', 418 | ); 419 | 420 | expect(document).toMatchSnapshot(); 421 | }); 422 | 423 | test('before declaration (with semicolon)', () => { 424 | let document = parseJs( 425 | 'let Component = styled.div`${borderWidth}${borderWidth};color: red`;', 426 | ); 427 | 428 | expect(document).toMatchSnapshot(); 429 | }); 430 | 431 | test('before declaration (with semicolon and space)', () => { 432 | let document = parseJs( 433 | 'let Component = styled.div`${borderWidth}${borderWidth}; color: red`;', 434 | ); 435 | 436 | expect(document).toMatchSnapshot(); 437 | }); 438 | 439 | test('between two declarations (no semicolon)', () => { 440 | let document = parseJs( 441 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth} border-color: blue;`;', 442 | ); 443 | 444 | expect(document).toMatchSnapshot(); 445 | }); 446 | 447 | test('between two declarations (with semicolon)', () => { 448 | let document = parseJs( 449 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth}; border-color: blue;`;', 450 | ); 451 | 452 | expect(document).toMatchSnapshot(); 453 | }); 454 | 455 | test('before comment (no semicolon, with space)', () => { 456 | let document = parseJs( 457 | 'let Component = styled.div`${borderWidth}${background} /* hello */`;', 458 | ); 459 | 460 | expect(document).toMatchSnapshot(); 461 | }); 462 | 463 | test('as the only content (no semicolon)', () => { 464 | let document = parseJs('let Component = styled.div`${color}${color}`;'); 465 | 466 | expect(document).toMatchSnapshot(); 467 | }); 468 | 469 | test('as the only content (with semicolon)', () => { 470 | let document = parseJs('let Component = styled.div`${color}${color};`;'); 471 | 472 | expect(document).toMatchSnapshot(); 473 | }); 474 | }); 475 | }); 476 | 477 | describe('interpolations with css helper (one level deep)', () => { 478 | test('single at the end', () => { 479 | let document = parseJs('let Component = styled.div`color: green;${css`color: red`}`;'); 480 | 481 | expect(document).toMatchSnapshot(); 482 | }); 483 | 484 | test('single at the start', () => { 485 | let document = parseJs('let Component = styled.div`${css`color: red`} color: green;`;'); 486 | 487 | expect(document).toMatchSnapshot(); 488 | }); 489 | 490 | test('single at the start and at the end', () => { 491 | let document = parseJs( 492 | 'let Component = styled.div`${css`color: red`} color: green;${css`color: blue`}`;', 493 | ); 494 | 495 | expect(document).toMatchSnapshot(); 496 | }); 497 | 498 | test('only two interpolations', () => { 499 | let document = parseJs( 500 | 'let Component = styled.div`${css`color: red`}${css`color: blue`}`;', 501 | ); 502 | 503 | expect(document).toMatchSnapshot(); 504 | }); 505 | }); 506 | 507 | describe('interpolations with css helper (many levels deep)', () => { 508 | test('two levels deep at the end', () => { 509 | let document = parseJs( 510 | 'let Component = styled.div`${css`color: red;${css`color: blue`}`}`;', 511 | ); 512 | 513 | expect(document).toMatchSnapshot(); 514 | }); 515 | 516 | test('three levels deep at the end', () => { 517 | let document = parseJs( 518 | 'let Component = styled.div`${css`color: red;${css`color: blue;${css`color: green;`}`}`}`;', 519 | ); 520 | 521 | expect(document).toMatchSnapshot(); 522 | }); 523 | 524 | test('two levels at the beginning', () => { 525 | let document = parseJs( 526 | 'let Component = styled.div`${css`${css`color: blue`} color: red;`}`;', 527 | ); 528 | 529 | expect(document).toMatchSnapshot(); 530 | }); 531 | }); 532 | 533 | describe('interpolations with props', () => { 534 | test('props with no styled helpers (at the end)', () => { 535 | let document = parseJs( 536 | 'let Component = styled.div`color: green;${props => `color: red`}`;', 537 | ); 538 | 539 | expect(document).toMatchSnapshot(); 540 | }); 541 | 542 | test('props with css helper (at the end)', () => { 543 | let document = parseJs( 544 | 'let Component = styled.div`color: green;${props => css`color: red`}`;', 545 | ); 546 | 547 | expect(document).toMatchSnapshot(); 548 | }); 549 | 550 | test('props with no styled helpers (at the beginning)', () => { 551 | let document = parseJs( 552 | 'let Component = styled.div`${props => `color: red`} color: green;`;', 553 | ); 554 | 555 | expect(document).toMatchSnapshot(); 556 | }); 557 | 558 | test('props with css helper (at the beginning)', () => { 559 | let document = parseJs( 560 | 'let Component = styled.div`${props => css`color: red`} color: green;`;', 561 | ); 562 | 563 | expect(document).toMatchSnapshot(); 564 | }); 565 | }); 566 | 567 | describe('component notations', () => { 568 | const notations = [ 569 | 'styled.foo', 570 | 'styled(Component)', 571 | 'styled.foo.attrs({})', 572 | 'styled(Component).attrs({})', 573 | 'css', 574 | 'createGlobalStyle', 575 | ]; 576 | 577 | test.each(notations)('%s', (notation) => { 578 | let document = parseJs('let Component = ' + notation + '`color: red;`;'); 579 | 580 | expect(document).toMatchSnapshot(); 581 | }); 582 | 583 | test('passing a function', () => { 584 | let document = parseJs('let Component = styled.div(props => `color: red;`);'); 585 | 586 | expect(document).toMatchSnapshot(); 587 | }); 588 | 589 | test('passing a function 2', () => { 590 | let document = parseJs("let Component = styled('div')(props => `color: red;`);"); 591 | 592 | expect(document).toMatchSnapshot(); 593 | }); 594 | }); 595 | 596 | describe('do not parse not component notations', () => { 597 | const notations = [ 598 | 'sstyled.foo', 599 | 'styledd(Component)', 600 | 'styledd.foo.attrs({})', 601 | 'sstyled(Component).attrs({})', 602 | 'csss', 603 | 'ccreateGlobalStyle', 604 | ]; 605 | 606 | test.each(notations)('%s', (notation) => { 607 | let document = parseJs('let Component = ' + notation + '`color: red;`;'); 608 | 609 | expect(document).toEqual([]); 610 | }); 611 | }); 612 | 613 | test('supports TypeScript', () => { 614 | let document = parseJs('let Component = styled.div<{ isDisabled?: boolean; }>`color: red;`;'); 615 | 616 | expect(document).toMatchSnapshot(); 617 | }); 618 | 619 | test('component after a component with interpolation', () => { 620 | let document = parseJs( 621 | 'const Component = styled.div`\n${css`position: sticky;`}\n`;\nconst Trigger = styled.div``;', 622 | ); 623 | 624 | expect(document).toMatchSnapshot(); 625 | }); 626 | 627 | test('does not crash for invalid JavaScript syntax', () => { 628 | let document = parseJs('const Component = styled.div`position: sticky', { from: 'test.js' }); 629 | 630 | expect(document).toMatchSnapshot(); 631 | }); 632 | 633 | test('comment between tag function and template literal. without interpolation', () => { 634 | let document = parseJs( 635 | "const StyledLink2 = styled('a', { shouldForwardProp })/* css */ `color: red;`;", 636 | ); 637 | 638 | expect(document).toMatchSnapshot(); 639 | }); 640 | 641 | test('comment between tag function and template literal. with interpolation', () => { 642 | let document = parseJs( 643 | "const StyledLink2 = styled('a', { shouldForwardProp })/* css */ `color: ${red};`;", 644 | ); 645 | 646 | expect(document).toMatchSnapshot(); 647 | }); 648 | -------------------------------------------------------------------------------- /lib/__tests__/__snapshots__/parseJs.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`comment between tag function and template literal. with interpolation 1`] = ` 4 | [ 5 | { 6 | "css": "color: \${red};", 7 | "interpolationRanges": [ 8 | { 9 | "end": 78, 10 | "start": 72, 11 | }, 12 | ], 13 | "locationStart": { 14 | "column": 56, 15 | "line": 1, 16 | }, 17 | "rangeEnd": 79, 18 | "rangeStart": 65, 19 | }, 20 | ] 21 | `; 22 | 23 | exports[`comment between tag function and template literal. without interpolation 1`] = ` 24 | [ 25 | { 26 | "css": "color: red;", 27 | "interpolationRanges": [], 28 | "locationStart": { 29 | "column": 56, 30 | "line": 1, 31 | }, 32 | "rangeEnd": 76, 33 | "rangeStart": 65, 34 | }, 35 | ] 36 | `; 37 | 38 | exports[`component after a component with interpolation 1`] = ` 39 | [ 40 | { 41 | "css": " 42 | \${css\`position: sticky;\`} 43 | ", 44 | "interpolationRanges": [ 45 | { 46 | "end": 55, 47 | "start": 30, 48 | }, 49 | ], 50 | "locationStart": { 51 | "column": 30, 52 | "line": 1, 53 | }, 54 | "rangeEnd": 56, 55 | "rangeStart": 29, 56 | }, 57 | { 58 | "css": "position: sticky;", 59 | "interpolationRanges": [], 60 | "locationStart": { 61 | "column": 7, 62 | "line": 2, 63 | }, 64 | "rangeEnd": 53, 65 | "rangeStart": 36, 66 | }, 67 | { 68 | "css": "", 69 | "interpolationRanges": [], 70 | "locationStart": { 71 | "column": 28, 72 | "line": 4, 73 | }, 74 | "rangeEnd": 86, 75 | "rangeStart": 86, 76 | }, 77 | ] 78 | `; 79 | 80 | exports[`component notations createGlobalStyle 1`] = ` 81 | [ 82 | { 83 | "css": "color: red;", 84 | "interpolationRanges": [], 85 | "locationStart": { 86 | "column": 35, 87 | "line": 1, 88 | }, 89 | "rangeEnd": 45, 90 | "rangeStart": 34, 91 | }, 92 | ] 93 | `; 94 | 95 | exports[`component notations css 1`] = ` 96 | [ 97 | { 98 | "css": "color: red;", 99 | "interpolationRanges": [], 100 | "locationStart": { 101 | "column": 21, 102 | "line": 1, 103 | }, 104 | "rangeEnd": 31, 105 | "rangeStart": 20, 106 | }, 107 | ] 108 | `; 109 | 110 | exports[`component notations passing a function 1`] = ` 111 | [ 112 | { 113 | "css": "color: red;", 114 | "interpolationRanges": [], 115 | "locationStart": { 116 | "column": 37, 117 | "line": 1, 118 | }, 119 | "rangeEnd": 48, 120 | "rangeStart": 37, 121 | }, 122 | ] 123 | `; 124 | 125 | exports[`component notations passing a function 2 1`] = ` 126 | [ 127 | { 128 | "css": "color: red;", 129 | "interpolationRanges": [], 130 | "locationStart": { 131 | "column": 40, 132 | "line": 1, 133 | }, 134 | "rangeEnd": 51, 135 | "rangeStart": 40, 136 | }, 137 | ] 138 | `; 139 | 140 | exports[`component notations styled(Component) 1`] = ` 141 | [ 142 | { 143 | "css": "color: red;", 144 | "interpolationRanges": [], 145 | "locationStart": { 146 | "column": 35, 147 | "line": 1, 148 | }, 149 | "rangeEnd": 45, 150 | "rangeStart": 34, 151 | }, 152 | ] 153 | `; 154 | 155 | exports[`component notations styled(Component).attrs({}) 1`] = ` 156 | [ 157 | { 158 | "css": "color: red;", 159 | "interpolationRanges": [], 160 | "locationStart": { 161 | "column": 45, 162 | "line": 1, 163 | }, 164 | "rangeEnd": 55, 165 | "rangeStart": 44, 166 | }, 167 | ] 168 | `; 169 | 170 | exports[`component notations styled.foo 1`] = ` 171 | [ 172 | { 173 | "css": "color: red;", 174 | "interpolationRanges": [], 175 | "locationStart": { 176 | "column": 28, 177 | "line": 1, 178 | }, 179 | "rangeEnd": 38, 180 | "rangeStart": 27, 181 | }, 182 | ] 183 | `; 184 | 185 | exports[`component notations styled.foo.attrs({}) 1`] = ` 186 | [ 187 | { 188 | "css": "color: red;", 189 | "interpolationRanges": [], 190 | "locationStart": { 191 | "column": 38, 192 | "line": 1, 193 | }, 194 | "rangeEnd": 48, 195 | "rangeStart": 37, 196 | }, 197 | ] 198 | `; 199 | 200 | exports[`does not crash for invalid JavaScript syntax 1`] = `[]`; 201 | 202 | exports[`interpolations with css helper (many levels deep) three levels deep at the end 1`] = ` 203 | [ 204 | { 205 | "css": "\${css\`color: red;\${css\`color: blue;\${css\`color: green;\`}\`}\`}", 206 | "interpolationRanges": [ 207 | { 208 | "end": 87, 209 | "start": 27, 210 | }, 211 | ], 212 | "locationStart": { 213 | "column": 28, 214 | "line": 1, 215 | }, 216 | "rangeEnd": 87, 217 | "rangeStart": 27, 218 | }, 219 | { 220 | "css": "color: red;\${css\`color: blue;\${css\`color: green;\`}\`}", 221 | "interpolationRanges": [ 222 | { 223 | "end": 85, 224 | "start": 44, 225 | }, 226 | ], 227 | "locationStart": { 228 | "column": 34, 229 | "line": 1, 230 | }, 231 | "rangeEnd": 85, 232 | "rangeStart": 33, 233 | }, 234 | { 235 | "css": "color: blue;\${css\`color: green;\`}", 236 | "interpolationRanges": [ 237 | { 238 | "end": 83, 239 | "start": 62, 240 | }, 241 | ], 242 | "locationStart": { 243 | "column": 51, 244 | "line": 1, 245 | }, 246 | "rangeEnd": 83, 247 | "rangeStart": 50, 248 | }, 249 | { 250 | "css": "color: green;", 251 | "interpolationRanges": [], 252 | "locationStart": { 253 | "column": 69, 254 | "line": 1, 255 | }, 256 | "rangeEnd": 81, 257 | "rangeStart": 68, 258 | }, 259 | ] 260 | `; 261 | 262 | exports[`interpolations with css helper (many levels deep) two levels at the beginning 1`] = ` 263 | [ 264 | { 265 | "css": "\${css\`\${css\`color: blue\`} color: red;\`}", 266 | "interpolationRanges": [ 267 | { 268 | "end": 66, 269 | "start": 27, 270 | }, 271 | ], 272 | "locationStart": { 273 | "column": 28, 274 | "line": 1, 275 | }, 276 | "rangeEnd": 66, 277 | "rangeStart": 27, 278 | }, 279 | { 280 | "css": "\${css\`color: blue\`} color: red;", 281 | "interpolationRanges": [ 282 | { 283 | "end": 52, 284 | "start": 33, 285 | }, 286 | ], 287 | "locationStart": { 288 | "column": 34, 289 | "line": 1, 290 | }, 291 | "rangeEnd": 64, 292 | "rangeStart": 33, 293 | }, 294 | { 295 | "css": "color: blue", 296 | "interpolationRanges": [], 297 | "locationStart": { 298 | "column": 40, 299 | "line": 1, 300 | }, 301 | "rangeEnd": 50, 302 | "rangeStart": 39, 303 | }, 304 | ] 305 | `; 306 | 307 | exports[`interpolations with css helper (many levels deep) two levels deep at the end 1`] = ` 308 | [ 309 | { 310 | "css": "\${css\`color: red;\${css\`color: blue\`}\`}", 311 | "interpolationRanges": [ 312 | { 313 | "end": 65, 314 | "start": 27, 315 | }, 316 | ], 317 | "locationStart": { 318 | "column": 28, 319 | "line": 1, 320 | }, 321 | "rangeEnd": 65, 322 | "rangeStart": 27, 323 | }, 324 | { 325 | "css": "color: red;\${css\`color: blue\`}", 326 | "interpolationRanges": [ 327 | { 328 | "end": 63, 329 | "start": 44, 330 | }, 331 | ], 332 | "locationStart": { 333 | "column": 34, 334 | "line": 1, 335 | }, 336 | "rangeEnd": 63, 337 | "rangeStart": 33, 338 | }, 339 | { 340 | "css": "color: blue", 341 | "interpolationRanges": [], 342 | "locationStart": { 343 | "column": 51, 344 | "line": 1, 345 | }, 346 | "rangeEnd": 61, 347 | "rangeStart": 50, 348 | }, 349 | ] 350 | `; 351 | 352 | exports[`interpolations with css helper (one level deep) only two interpolations 1`] = ` 353 | [ 354 | { 355 | "css": "\${css\`color: red\`}\${css\`color: blue\`}", 356 | "interpolationRanges": [ 357 | { 358 | "end": 45, 359 | "start": 27, 360 | }, 361 | { 362 | "end": 64, 363 | "start": 45, 364 | }, 365 | ], 366 | "locationStart": { 367 | "column": 28, 368 | "line": 1, 369 | }, 370 | "rangeEnd": 64, 371 | "rangeStart": 27, 372 | }, 373 | { 374 | "css": "color: red", 375 | "interpolationRanges": [], 376 | "locationStart": { 377 | "column": 34, 378 | "line": 1, 379 | }, 380 | "rangeEnd": 43, 381 | "rangeStart": 33, 382 | }, 383 | { 384 | "css": "color: blue", 385 | "interpolationRanges": [], 386 | "locationStart": { 387 | "column": 52, 388 | "line": 1, 389 | }, 390 | "rangeEnd": 62, 391 | "rangeStart": 51, 392 | }, 393 | ] 394 | `; 395 | 396 | exports[`interpolations with css helper (one level deep) single at the end 1`] = ` 397 | [ 398 | { 399 | "css": "color: green;\${css\`color: red\`}", 400 | "interpolationRanges": [ 401 | { 402 | "end": 58, 403 | "start": 40, 404 | }, 405 | ], 406 | "locationStart": { 407 | "column": 28, 408 | "line": 1, 409 | }, 410 | "rangeEnd": 58, 411 | "rangeStart": 27, 412 | }, 413 | { 414 | "css": "color: red", 415 | "interpolationRanges": [], 416 | "locationStart": { 417 | "column": 47, 418 | "line": 1, 419 | }, 420 | "rangeEnd": 56, 421 | "rangeStart": 46, 422 | }, 423 | ] 424 | `; 425 | 426 | exports[`interpolations with css helper (one level deep) single at the start 1`] = ` 427 | [ 428 | { 429 | "css": "\${css\`color: red\`} color: green;", 430 | "interpolationRanges": [ 431 | { 432 | "end": 45, 433 | "start": 27, 434 | }, 435 | ], 436 | "locationStart": { 437 | "column": 28, 438 | "line": 1, 439 | }, 440 | "rangeEnd": 59, 441 | "rangeStart": 27, 442 | }, 443 | { 444 | "css": "color: red", 445 | "interpolationRanges": [], 446 | "locationStart": { 447 | "column": 34, 448 | "line": 1, 449 | }, 450 | "rangeEnd": 43, 451 | "rangeStart": 33, 452 | }, 453 | ] 454 | `; 455 | 456 | exports[`interpolations with css helper (one level deep) single at the start and at the end 1`] = ` 457 | [ 458 | { 459 | "css": "\${css\`color: red\`} color: green;\${css\`color: blue\`}", 460 | "interpolationRanges": [ 461 | { 462 | "end": 45, 463 | "start": 27, 464 | }, 465 | { 466 | "end": 78, 467 | "start": 59, 468 | }, 469 | ], 470 | "locationStart": { 471 | "column": 28, 472 | "line": 1, 473 | }, 474 | "rangeEnd": 78, 475 | "rangeStart": 27, 476 | }, 477 | { 478 | "css": "color: red", 479 | "interpolationRanges": [], 480 | "locationStart": { 481 | "column": 34, 482 | "line": 1, 483 | }, 484 | "rangeEnd": 43, 485 | "rangeStart": 33, 486 | }, 487 | { 488 | "css": "color: blue", 489 | "interpolationRanges": [], 490 | "locationStart": { 491 | "column": 66, 492 | "line": 1, 493 | }, 494 | "rangeEnd": 76, 495 | "rangeStart": 65, 496 | }, 497 | ] 498 | `; 499 | 500 | exports[`interpolations with props props with css helper (at the beginning) 1`] = ` 501 | [ 502 | { 503 | "css": "\${props => css\`color: red\`} color: green;", 504 | "interpolationRanges": [ 505 | { 506 | "end": 54, 507 | "start": 27, 508 | }, 509 | ], 510 | "locationStart": { 511 | "column": 28, 512 | "line": 1, 513 | }, 514 | "rangeEnd": 68, 515 | "rangeStart": 27, 516 | }, 517 | { 518 | "css": "color: red", 519 | "interpolationRanges": [], 520 | "locationStart": { 521 | "column": 43, 522 | "line": 1, 523 | }, 524 | "rangeEnd": 52, 525 | "rangeStart": 42, 526 | }, 527 | ] 528 | `; 529 | 530 | exports[`interpolations with props props with css helper (at the end) 1`] = ` 531 | [ 532 | { 533 | "css": "color: green;\${props => css\`color: red\`}", 534 | "interpolationRanges": [ 535 | { 536 | "end": 67, 537 | "start": 40, 538 | }, 539 | ], 540 | "locationStart": { 541 | "column": 28, 542 | "line": 1, 543 | }, 544 | "rangeEnd": 67, 545 | "rangeStart": 27, 546 | }, 547 | { 548 | "css": "color: red", 549 | "interpolationRanges": [], 550 | "locationStart": { 551 | "column": 56, 552 | "line": 1, 553 | }, 554 | "rangeEnd": 65, 555 | "rangeStart": 55, 556 | }, 557 | ] 558 | `; 559 | 560 | exports[`interpolations with props props with no styled helpers (at the beginning) 1`] = ` 561 | [ 562 | { 563 | "css": "\${props => \`color: red\`} color: green;", 564 | "interpolationRanges": [ 565 | { 566 | "end": 51, 567 | "start": 27, 568 | }, 569 | ], 570 | "locationStart": { 571 | "column": 28, 572 | "line": 1, 573 | }, 574 | "rangeEnd": 65, 575 | "rangeStart": 27, 576 | }, 577 | ] 578 | `; 579 | 580 | exports[`interpolations with props props with no styled helpers (at the end) 1`] = ` 581 | [ 582 | { 583 | "css": "color: green;\${props => \`color: red\`}", 584 | "interpolationRanges": [ 585 | { 586 | "end": 64, 587 | "start": 40, 588 | }, 589 | ], 590 | "locationStart": { 591 | "column": 28, 592 | "line": 1, 593 | }, 594 | "rangeEnd": 64, 595 | "rangeStart": 27, 596 | }, 597 | ] 598 | `; 599 | 600 | exports[`no interpolations comment in selector 1`] = ` 601 | [ 602 | { 603 | "css": "a, /* hello */ b { color: red; }", 604 | "interpolationRanges": [], 605 | "locationStart": { 606 | "column": 28, 607 | "line": 1, 608 | }, 609 | "rangeEnd": 59, 610 | "rangeStart": 27, 611 | }, 612 | ] 613 | `; 614 | 615 | exports[`no interpolations empty component 1`] = ` 616 | [ 617 | { 618 | "css": "", 619 | "interpolationRanges": [], 620 | "locationStart": { 621 | "column": 28, 622 | "line": 1, 623 | }, 624 | "rangeEnd": 27, 625 | "rangeStart": 27, 626 | }, 627 | ] 628 | `; 629 | 630 | exports[`no interpolations empty file 1`] = `[]`; 631 | 632 | exports[`no interpolations escaped characters in the code 1`] = ` 633 | [ 634 | { 635 | "css": "content: "\\u200B";", 636 | "interpolationRanges": [], 637 | "locationStart": { 638 | "column": 28, 639 | "line": 1, 640 | }, 641 | "rangeEnd": 45, 642 | "rangeStart": 27, 643 | }, 644 | ] 645 | `; 646 | 647 | exports[`no interpolations no components in a file 1`] = `[]`; 648 | 649 | exports[`no interpolations one component 1`] = ` 650 | [ 651 | { 652 | "css": "color: red;", 653 | "interpolationRanges": [], 654 | "locationStart": { 655 | "column": 28, 656 | "line": 1, 657 | }, 658 | "rangeEnd": 38, 659 | "rangeStart": 27, 660 | }, 661 | ] 662 | `; 663 | 664 | exports[`no interpolations property on its row. extra space in the begining 1`] = ` 665 | [ 666 | { 667 | "css": " 668 | color: red; 669 | ", 670 | "interpolationRanges": [], 671 | "locationStart": { 672 | "column": 28, 673 | "line": 1, 674 | }, 675 | "rangeEnd": 42, 676 | "rangeStart": 27, 677 | }, 678 | ] 679 | `; 680 | 681 | exports[`no interpolations property on its row. no extra space 1`] = ` 682 | [ 683 | { 684 | "css": " 685 | color: red; 686 | ", 687 | "interpolationRanges": [], 688 | "locationStart": { 689 | "column": 28, 690 | "line": 1, 691 | }, 692 | "rangeEnd": 41, 693 | "rangeStart": 27, 694 | }, 695 | ] 696 | `; 697 | 698 | exports[`no interpolations selector could be on multiple lines 1`] = ` 699 | [ 700 | { 701 | "css": "a, 702 | b { color: red; }", 703 | "interpolationRanges": [], 704 | "locationStart": { 705 | "column": 28, 706 | "line": 1, 707 | }, 708 | "rangeEnd": 48, 709 | "rangeStart": 27, 710 | }, 711 | ] 712 | `; 713 | 714 | exports[`no interpolations two components 1`] = ` 715 | [ 716 | { 717 | "css": "color: red;", 718 | "interpolationRanges": [], 719 | "locationStart": { 720 | "column": 28, 721 | "line": 1, 722 | }, 723 | "rangeEnd": 38, 724 | "rangeStart": 27, 725 | }, 726 | { 727 | "css": "border-color: blue", 728 | "interpolationRanges": [], 729 | "locationStart": { 730 | "column": 28, 731 | "line": 2, 732 | }, 733 | "rangeEnd": 86, 734 | "rangeStart": 68, 735 | }, 736 | ] 737 | `; 738 | 739 | exports[`simple interpolations multiple standalone interpolations after declaration (no semicolon, no space after) 1`] = ` 740 | [ 741 | { 742 | "css": "color: red; \${borderWidth}\${borderWidth}", 743 | "interpolationRanges": [ 744 | { 745 | "end": 53, 746 | "start": 39, 747 | }, 748 | { 749 | "end": 67, 750 | "start": 53, 751 | }, 752 | ], 753 | "locationStart": { 754 | "column": 28, 755 | "line": 1, 756 | }, 757 | "rangeEnd": 67, 758 | "rangeStart": 27, 759 | }, 760 | ] 761 | `; 762 | 763 | exports[`simple interpolations multiple standalone interpolations after declaration (no semicolon, space after) 1`] = ` 764 | [ 765 | { 766 | "css": "color: red; \${borderWidth} \${borderWidth} ", 767 | "interpolationRanges": [ 768 | { 769 | "end": 53, 770 | "start": 39, 771 | }, 772 | { 773 | "end": 68, 774 | "start": 54, 775 | }, 776 | ], 777 | "locationStart": { 778 | "column": 28, 779 | "line": 1, 780 | }, 781 | "rangeEnd": 69, 782 | "rangeStart": 27, 783 | }, 784 | ] 785 | `; 786 | 787 | exports[`simple interpolations multiple standalone interpolations after declaration (with semicolon) 1`] = ` 788 | [ 789 | { 790 | "css": "color: red; \${borderWidth} \${borderWidth};", 791 | "interpolationRanges": [ 792 | { 793 | "end": 53, 794 | "start": 39, 795 | }, 796 | { 797 | "end": 68, 798 | "start": 54, 799 | }, 800 | ], 801 | "locationStart": { 802 | "column": 28, 803 | "line": 1, 804 | }, 805 | "rangeEnd": 69, 806 | "rangeStart": 27, 807 | }, 808 | ] 809 | `; 810 | 811 | exports[`simple interpolations multiple standalone interpolations as the only content (no semicolon) 1`] = ` 812 | [ 813 | { 814 | "css": "\${color}\${color}", 815 | "interpolationRanges": [ 816 | { 817 | "end": 35, 818 | "start": 27, 819 | }, 820 | { 821 | "end": 43, 822 | "start": 35, 823 | }, 824 | ], 825 | "locationStart": { 826 | "column": 28, 827 | "line": 1, 828 | }, 829 | "rangeEnd": 43, 830 | "rangeStart": 27, 831 | }, 832 | ] 833 | `; 834 | 835 | exports[`simple interpolations multiple standalone interpolations as the only content (with semicolon) 1`] = ` 836 | [ 837 | { 838 | "css": "\${color}\${color};", 839 | "interpolationRanges": [ 840 | { 841 | "end": 35, 842 | "start": 27, 843 | }, 844 | { 845 | "end": 43, 846 | "start": 35, 847 | }, 848 | ], 849 | "locationStart": { 850 | "column": 28, 851 | "line": 1, 852 | }, 853 | "rangeEnd": 44, 854 | "rangeStart": 27, 855 | }, 856 | ] 857 | `; 858 | 859 | exports[`simple interpolations multiple standalone interpolations before comment (no semicolon, with space) 1`] = ` 860 | [ 861 | { 862 | "css": "\${borderWidth}\${background} /* hello */", 863 | "interpolationRanges": [ 864 | { 865 | "end": 41, 866 | "start": 27, 867 | }, 868 | { 869 | "end": 54, 870 | "start": 41, 871 | }, 872 | ], 873 | "locationStart": { 874 | "column": 28, 875 | "line": 1, 876 | }, 877 | "rangeEnd": 66, 878 | "rangeStart": 27, 879 | }, 880 | ] 881 | `; 882 | 883 | exports[`simple interpolations multiple standalone interpolations before declaration (no semicolon, no space) 1`] = ` 884 | [ 885 | { 886 | "css": "\${borderWidth} \${background}color: red", 887 | "interpolationRanges": [ 888 | { 889 | "end": 41, 890 | "start": 27, 891 | }, 892 | { 893 | "end": 55, 894 | "start": 42, 895 | }, 896 | ], 897 | "locationStart": { 898 | "column": 28, 899 | "line": 1, 900 | }, 901 | "rangeEnd": 65, 902 | "rangeStart": 27, 903 | }, 904 | ] 905 | `; 906 | 907 | exports[`simple interpolations multiple standalone interpolations before declaration (no semicolon, with space) 1`] = ` 908 | [ 909 | { 910 | "css": "\${borderWidth}\${background} color: red", 911 | "interpolationRanges": [ 912 | { 913 | "end": 41, 914 | "start": 27, 915 | }, 916 | { 917 | "end": 54, 918 | "start": 41, 919 | }, 920 | ], 921 | "locationStart": { 922 | "column": 28, 923 | "line": 1, 924 | }, 925 | "rangeEnd": 65, 926 | "rangeStart": 27, 927 | }, 928 | ] 929 | `; 930 | 931 | exports[`simple interpolations multiple standalone interpolations before declaration (no semicolon, with space, three interpolations) 1`] = ` 932 | [ 933 | { 934 | "css": "\${borderWidth}\${background}\${display} color: red", 935 | "interpolationRanges": [ 936 | { 937 | "end": 41, 938 | "start": 27, 939 | }, 940 | { 941 | "end": 54, 942 | "start": 41, 943 | }, 944 | { 945 | "end": 64, 946 | "start": 54, 947 | }, 948 | ], 949 | "locationStart": { 950 | "column": 28, 951 | "line": 1, 952 | }, 953 | "rangeEnd": 75, 954 | "rangeStart": 27, 955 | }, 956 | ] 957 | `; 958 | 959 | exports[`simple interpolations multiple standalone interpolations before declaration (with semicolon and space) 1`] = ` 960 | [ 961 | { 962 | "css": "\${borderWidth}\${borderWidth}; color: red", 963 | "interpolationRanges": [ 964 | { 965 | "end": 41, 966 | "start": 27, 967 | }, 968 | { 969 | "end": 55, 970 | "start": 41, 971 | }, 972 | ], 973 | "locationStart": { 974 | "column": 28, 975 | "line": 1, 976 | }, 977 | "rangeEnd": 67, 978 | "rangeStart": 27, 979 | }, 980 | ] 981 | `; 982 | 983 | exports[`simple interpolations multiple standalone interpolations before declaration (with semicolon) 1`] = ` 984 | [ 985 | { 986 | "css": "\${borderWidth}\${borderWidth};color: red", 987 | "interpolationRanges": [ 988 | { 989 | "end": 41, 990 | "start": 27, 991 | }, 992 | { 993 | "end": 55, 994 | "start": 41, 995 | }, 996 | ], 997 | "locationStart": { 998 | "column": 28, 999 | "line": 1, 1000 | }, 1001 | "rangeEnd": 66, 1002 | "rangeStart": 27, 1003 | }, 1004 | ] 1005 | `; 1006 | 1007 | exports[`simple interpolations multiple standalone interpolations before declaration (with space) 1`] = ` 1008 | [ 1009 | { 1010 | "css": "\${borderWidth} \${borderWidth} color: red", 1011 | "interpolationRanges": [ 1012 | { 1013 | "end": 41, 1014 | "start": 27, 1015 | }, 1016 | { 1017 | "end": 56, 1018 | "start": 42, 1019 | }, 1020 | ], 1021 | "locationStart": { 1022 | "column": 28, 1023 | "line": 1, 1024 | }, 1025 | "rangeEnd": 67, 1026 | "rangeStart": 27, 1027 | }, 1028 | ] 1029 | `; 1030 | 1031 | exports[`simple interpolations multiple standalone interpolations between two declarations (no semicolon) 1`] = ` 1032 | [ 1033 | { 1034 | "css": "color: red; \${borderWidth} \${borderWidth} border-color: blue;", 1035 | "interpolationRanges": [ 1036 | { 1037 | "end": 53, 1038 | "start": 39, 1039 | }, 1040 | { 1041 | "end": 68, 1042 | "start": 54, 1043 | }, 1044 | ], 1045 | "locationStart": { 1046 | "column": 28, 1047 | "line": 1, 1048 | }, 1049 | "rangeEnd": 88, 1050 | "rangeStart": 27, 1051 | }, 1052 | ] 1053 | `; 1054 | 1055 | exports[`simple interpolations multiple standalone interpolations between two declarations (with semicolon) 1`] = ` 1056 | [ 1057 | { 1058 | "css": "color: red; \${borderWidth} \${borderWidth}; border-color: blue;", 1059 | "interpolationRanges": [ 1060 | { 1061 | "end": 53, 1062 | "start": 39, 1063 | }, 1064 | { 1065 | "end": 68, 1066 | "start": 54, 1067 | }, 1068 | ], 1069 | "locationStart": { 1070 | "column": 28, 1071 | "line": 1, 1072 | }, 1073 | "rangeEnd": 89, 1074 | "rangeStart": 27, 1075 | }, 1076 | ] 1077 | `; 1078 | 1079 | exports[`simple interpolations properties property name (first property) 1`] = ` 1080 | [ 1081 | { 1082 | "css": "\${color}: red", 1083 | "interpolationRanges": [ 1084 | { 1085 | "end": 35, 1086 | "start": 27, 1087 | }, 1088 | ], 1089 | "locationStart": { 1090 | "column": 28, 1091 | "line": 1, 1092 | }, 1093 | "rangeEnd": 40, 1094 | "rangeStart": 27, 1095 | }, 1096 | ] 1097 | `; 1098 | 1099 | exports[`simple interpolations properties property name (second property) 1`] = ` 1100 | [ 1101 | { 1102 | "css": "display: flex; \${color}: red", 1103 | "interpolationRanges": [ 1104 | { 1105 | "end": 50, 1106 | "start": 42, 1107 | }, 1108 | ], 1109 | "locationStart": { 1110 | "column": 28, 1111 | "line": 1, 1112 | }, 1113 | "rangeEnd": 55, 1114 | "rangeStart": 27, 1115 | }, 1116 | ] 1117 | `; 1118 | 1119 | exports[`simple interpolations properties property value (no semicolon) 1`] = ` 1120 | [ 1121 | { 1122 | "css": "color: \${red}", 1123 | "interpolationRanges": [ 1124 | { 1125 | "end": 40, 1126 | "start": 34, 1127 | }, 1128 | ], 1129 | "locationStart": { 1130 | "column": 28, 1131 | "line": 1, 1132 | }, 1133 | "rangeEnd": 40, 1134 | "rangeStart": 27, 1135 | }, 1136 | ] 1137 | `; 1138 | 1139 | exports[`simple interpolations properties property value (with semicolon) 1`] = ` 1140 | [ 1141 | { 1142 | "css": "color: \${red};", 1143 | "interpolationRanges": [ 1144 | { 1145 | "end": 40, 1146 | "start": 34, 1147 | }, 1148 | ], 1149 | "locationStart": { 1150 | "column": 28, 1151 | "line": 1, 1152 | }, 1153 | "rangeEnd": 41, 1154 | "rangeStart": 27, 1155 | }, 1156 | ] 1157 | `; 1158 | 1159 | exports[`simple interpolations properties property value and !important (no semicolon) 1`] = ` 1160 | [ 1161 | { 1162 | "css": "color: \${red} !important", 1163 | "interpolationRanges": [ 1164 | { 1165 | "end": 40, 1166 | "start": 34, 1167 | }, 1168 | ], 1169 | "locationStart": { 1170 | "column": 28, 1171 | "line": 1, 1172 | }, 1173 | "rangeEnd": 51, 1174 | "rangeStart": 27, 1175 | }, 1176 | ] 1177 | `; 1178 | 1179 | exports[`simple interpolations properties property value and !important (with semicolon) 1`] = ` 1180 | [ 1181 | { 1182 | "css": "color: \${red} !important;", 1183 | "interpolationRanges": [ 1184 | { 1185 | "end": 40, 1186 | "start": 34, 1187 | }, 1188 | ], 1189 | "locationStart": { 1190 | "column": 28, 1191 | "line": 1, 1192 | }, 1193 | "rangeEnd": 52, 1194 | "rangeStart": 27, 1195 | }, 1196 | ] 1197 | `; 1198 | 1199 | exports[`simple interpolations properties property value with interpolation inside url function 1`] = ` 1200 | [ 1201 | { 1202 | "css": "background-image: url(\${imageUrl})", 1203 | "interpolationRanges": [ 1204 | { 1205 | "end": 60, 1206 | "start": 49, 1207 | }, 1208 | ], 1209 | "locationStart": { 1210 | "column": 28, 1211 | "line": 1, 1212 | }, 1213 | "rangeEnd": 61, 1214 | "rangeStart": 27, 1215 | }, 1216 | ] 1217 | `; 1218 | 1219 | exports[`simple interpolations properties property value with props interpolation 1`] = ` 1220 | [ 1221 | { 1222 | "css": "background-image: \${(props) => props.backgroundImage}", 1223 | "interpolationRanges": [ 1224 | { 1225 | "end": 80, 1226 | "start": 45, 1227 | }, 1228 | ], 1229 | "locationStart": { 1230 | "column": 28, 1231 | "line": 1, 1232 | }, 1233 | "rangeEnd": 80, 1234 | "rangeStart": 27, 1235 | }, 1236 | ] 1237 | `; 1238 | 1239 | exports[`simple interpolations properties property value with props interpolation inside url function 1`] = ` 1240 | [ 1241 | { 1242 | "css": "background-image: url(\${(props) => props.backgroundImage})", 1243 | "interpolationRanges": [ 1244 | { 1245 | "end": 84, 1246 | "start": 49, 1247 | }, 1248 | ], 1249 | "locationStart": { 1250 | "column": 28, 1251 | "line": 1, 1252 | }, 1253 | "rangeEnd": 85, 1254 | "rangeStart": 27, 1255 | }, 1256 | ] 1257 | `; 1258 | 1259 | exports[`simple interpolations properties property value with symbol right before interpolation 1`] = ` 1260 | [ 1261 | { 1262 | "css": "margin: -\${space}", 1263 | "interpolationRanges": [ 1264 | { 1265 | "end": 44, 1266 | "start": 36, 1267 | }, 1268 | ], 1269 | "locationStart": { 1270 | "column": 28, 1271 | "line": 1, 1272 | }, 1273 | "rangeEnd": 44, 1274 | "rangeStart": 27, 1275 | }, 1276 | ] 1277 | `; 1278 | 1279 | exports[`simple interpolations properties property value with two interpolations 1`] = ` 1280 | [ 1281 | { 1282 | "css": "box-shadow: \${elevation1}, \${elevation2}", 1283 | "interpolationRanges": [ 1284 | { 1285 | "end": 52, 1286 | "start": 39, 1287 | }, 1288 | { 1289 | "end": 67, 1290 | "start": 54, 1291 | }, 1292 | ], 1293 | "locationStart": { 1294 | "column": 28, 1295 | "line": 1, 1296 | }, 1297 | "rangeEnd": 67, 1298 | "rangeStart": 27, 1299 | }, 1300 | ] 1301 | `; 1302 | 1303 | exports[`simple interpolations properties property value. interpolation with a comment after value 1`] = ` 1304 | [ 1305 | { 1306 | "css": "color: \${red /* hello */}", 1307 | "interpolationRanges": [ 1308 | { 1309 | "end": 52, 1310 | "start": 34, 1311 | }, 1312 | ], 1313 | "locationStart": { 1314 | "column": 28, 1315 | "line": 1, 1316 | }, 1317 | "rangeEnd": 52, 1318 | "rangeStart": 27, 1319 | }, 1320 | ] 1321 | `; 1322 | 1323 | exports[`simple interpolations properties property value. interpolation with a comment before value 1`] = ` 1324 | [ 1325 | { 1326 | "css": "color: \${/* hello */ red}", 1327 | "interpolationRanges": [ 1328 | { 1329 | "end": 52, 1330 | "start": 34, 1331 | }, 1332 | ], 1333 | "locationStart": { 1334 | "column": 28, 1335 | "line": 1, 1336 | }, 1337 | "rangeEnd": 52, 1338 | "rangeStart": 27, 1339 | }, 1340 | ] 1341 | `; 1342 | 1343 | exports[`simple interpolations selectors comment in selector with interpolation 1`] = ` 1344 | [ 1345 | { 1346 | "css": "\${Card}:hover, /* hello */ b { color: red; }", 1347 | "interpolationRanges": [ 1348 | { 1349 | "end": 34, 1350 | "start": 27, 1351 | }, 1352 | ], 1353 | "locationStart": { 1354 | "column": 28, 1355 | "line": 1, 1356 | }, 1357 | "rangeEnd": 71, 1358 | "rangeStart": 27, 1359 | }, 1360 | ] 1361 | `; 1362 | 1363 | exports[`simple interpolations selectors in selector (first part is interpolation, one selector) 1`] = ` 1364 | [ 1365 | { 1366 | "css": "\${Component} a { color: red; }", 1367 | "interpolationRanges": [ 1368 | { 1369 | "end": 39, 1370 | "start": 27, 1371 | }, 1372 | ], 1373 | "locationStart": { 1374 | "column": 28, 1375 | "line": 1, 1376 | }, 1377 | "rangeEnd": 57, 1378 | "rangeStart": 27, 1379 | }, 1380 | ] 1381 | `; 1382 | 1383 | exports[`simple interpolations selectors in selector (first selector is interpolation) 1`] = ` 1384 | [ 1385 | { 1386 | "css": "\${Component}, a { color: red; }", 1387 | "interpolationRanges": [ 1388 | { 1389 | "end": 39, 1390 | "start": 27, 1391 | }, 1392 | ], 1393 | "locationStart": { 1394 | "column": 28, 1395 | "line": 1, 1396 | }, 1397 | "rangeEnd": 58, 1398 | "rangeStart": 27, 1399 | }, 1400 | ] 1401 | `; 1402 | 1403 | exports[`simple interpolations selectors in selector (second part is interpolation, one selector) 1`] = ` 1404 | [ 1405 | { 1406 | "css": "a \${Component} { color: red; }", 1407 | "interpolationRanges": [ 1408 | { 1409 | "end": 41, 1410 | "start": 29, 1411 | }, 1412 | ], 1413 | "locationStart": { 1414 | "column": 28, 1415 | "line": 1, 1416 | }, 1417 | "rangeEnd": 57, 1418 | "rangeStart": 27, 1419 | }, 1420 | ] 1421 | `; 1422 | 1423 | exports[`simple interpolations selectors in selector (second selector is interpolation, two selectors) 1`] = ` 1424 | [ 1425 | { 1426 | "css": "a, \${Component} { color: red; }", 1427 | "interpolationRanges": [ 1428 | { 1429 | "end": 42, 1430 | "start": 30, 1431 | }, 1432 | ], 1433 | "locationStart": { 1434 | "column": 28, 1435 | "line": 1, 1436 | }, 1437 | "rangeEnd": 58, 1438 | "rangeStart": 27, 1439 | }, 1440 | ] 1441 | `; 1442 | 1443 | exports[`simple interpolations selectors in selector (whole selector starts with a dot) 1`] = ` 1444 | [ 1445 | { 1446 | "css": ".\${Component} { color: red; }", 1447 | "interpolationRanges": [ 1448 | { 1449 | "end": 40, 1450 | "start": 28, 1451 | }, 1452 | ], 1453 | "locationStart": { 1454 | "column": 28, 1455 | "line": 1, 1456 | }, 1457 | "rangeEnd": 56, 1458 | "rangeStart": 27, 1459 | }, 1460 | ] 1461 | `; 1462 | 1463 | exports[`simple interpolations selectors in selector (whole selector) 1`] = ` 1464 | [ 1465 | { 1466 | "css": "\${Component} { color: red; }", 1467 | "interpolationRanges": [ 1468 | { 1469 | "end": 39, 1470 | "start": 27, 1471 | }, 1472 | ], 1473 | "locationStart": { 1474 | "column": 28, 1475 | "line": 1, 1476 | }, 1477 | "rangeEnd": 55, 1478 | "rangeStart": 27, 1479 | }, 1480 | ] 1481 | `; 1482 | 1483 | exports[`simple interpolations selectors interpolation on a new line before selector 1`] = ` 1484 | [ 1485 | { 1486 | "css": "\${hello} 1487 | a { color: red; }", 1488 | "interpolationRanges": [ 1489 | { 1490 | "end": 35, 1491 | "start": 27, 1492 | }, 1493 | ], 1494 | "locationStart": { 1495 | "column": 28, 1496 | "line": 1, 1497 | }, 1498 | "rangeEnd": 54, 1499 | "rangeStart": 27, 1500 | }, 1501 | ] 1502 | `; 1503 | 1504 | exports[`simple interpolations selectors interpolation with semicolon before selector 1`] = ` 1505 | [ 1506 | { 1507 | "css": "\${Component}; a { color: red; }", 1508 | "interpolationRanges": [ 1509 | { 1510 | "end": 39, 1511 | "start": 27, 1512 | }, 1513 | ], 1514 | "locationStart": { 1515 | "column": 28, 1516 | "line": 1, 1517 | }, 1518 | "rangeEnd": 58, 1519 | "rangeStart": 27, 1520 | }, 1521 | ] 1522 | `; 1523 | 1524 | exports[`simple interpolations standalone interpolation after comment (no semicolon) 1`] = ` 1525 | [ 1526 | { 1527 | "css": "/* hello */ \${borderWidth}", 1528 | "interpolationRanges": [ 1529 | { 1530 | "end": 53, 1531 | "start": 39, 1532 | }, 1533 | ], 1534 | "locationStart": { 1535 | "column": 28, 1536 | "line": 1, 1537 | }, 1538 | "rangeEnd": 53, 1539 | "rangeStart": 27, 1540 | }, 1541 | ] 1542 | `; 1543 | 1544 | exports[`simple interpolations standalone interpolation after comment (with semicolon) 1`] = ` 1545 | [ 1546 | { 1547 | "css": "/* hello */ \${borderWidth};", 1548 | "interpolationRanges": [ 1549 | { 1550 | "end": 53, 1551 | "start": 39, 1552 | }, 1553 | ], 1554 | "locationStart": { 1555 | "column": 28, 1556 | "line": 1, 1557 | }, 1558 | "rangeEnd": 54, 1559 | "rangeStart": 27, 1560 | }, 1561 | ] 1562 | `; 1563 | 1564 | exports[`simple interpolations standalone interpolation after declaration (no semicolon, no space after) 1`] = ` 1565 | [ 1566 | { 1567 | "css": "color: red; \${borderWidth}", 1568 | "interpolationRanges": [ 1569 | { 1570 | "end": 53, 1571 | "start": 39, 1572 | }, 1573 | ], 1574 | "locationStart": { 1575 | "column": 28, 1576 | "line": 1, 1577 | }, 1578 | "rangeEnd": 53, 1579 | "rangeStart": 27, 1580 | }, 1581 | ] 1582 | `; 1583 | 1584 | exports[`simple interpolations standalone interpolation after declaration (no semicolon, space after) 1`] = ` 1585 | [ 1586 | { 1587 | "css": "color: red; \${borderWidth} ", 1588 | "interpolationRanges": [ 1589 | { 1590 | "end": 53, 1591 | "start": 39, 1592 | }, 1593 | ], 1594 | "locationStart": { 1595 | "column": 28, 1596 | "line": 1, 1597 | }, 1598 | "rangeEnd": 54, 1599 | "rangeStart": 27, 1600 | }, 1601 | ] 1602 | `; 1603 | 1604 | exports[`simple interpolations standalone interpolation after declaration (with semicolon) 1`] = ` 1605 | [ 1606 | { 1607 | "css": "color: red; \${borderWidth};", 1608 | "interpolationRanges": [ 1609 | { 1610 | "end": 53, 1611 | "start": 39, 1612 | }, 1613 | ], 1614 | "locationStart": { 1615 | "column": 28, 1616 | "line": 1, 1617 | }, 1618 | "rangeEnd": 54, 1619 | "rangeStart": 27, 1620 | }, 1621 | ] 1622 | `; 1623 | 1624 | exports[`simple interpolations standalone interpolation as the only content (no semicolon) 1`] = ` 1625 | [ 1626 | { 1627 | "css": "\${color}", 1628 | "interpolationRanges": [ 1629 | { 1630 | "end": 35, 1631 | "start": 27, 1632 | }, 1633 | ], 1634 | "locationStart": { 1635 | "column": 28, 1636 | "line": 1, 1637 | }, 1638 | "rangeEnd": 35, 1639 | "rangeStart": 27, 1640 | }, 1641 | ] 1642 | `; 1643 | 1644 | exports[`simple interpolations standalone interpolation as the only content (with semicolon) 1`] = ` 1645 | [ 1646 | { 1647 | "css": "\${color};", 1648 | "interpolationRanges": [ 1649 | { 1650 | "end": 35, 1651 | "start": 27, 1652 | }, 1653 | ], 1654 | "locationStart": { 1655 | "column": 28, 1656 | "line": 1, 1657 | }, 1658 | "rangeEnd": 36, 1659 | "rangeStart": 27, 1660 | }, 1661 | ] 1662 | `; 1663 | 1664 | exports[`simple interpolations standalone interpolation before at-rule (with semicolon) 1`] = ` 1665 | [ 1666 | { 1667 | "css": "\${borderWidth};@media screen {}", 1668 | "interpolationRanges": [ 1669 | { 1670 | "end": 41, 1671 | "start": 27, 1672 | }, 1673 | ], 1674 | "locationStart": { 1675 | "column": 28, 1676 | "line": 1, 1677 | }, 1678 | "rangeEnd": 58, 1679 | "rangeStart": 27, 1680 | }, 1681 | ] 1682 | `; 1683 | 1684 | exports[`simple interpolations standalone interpolation before at-rule (without semicolon) 1`] = ` 1685 | [ 1686 | { 1687 | "css": "\${borderWidth} @media screen {}", 1688 | "interpolationRanges": [ 1689 | { 1690 | "end": 41, 1691 | "start": 27, 1692 | }, 1693 | ], 1694 | "locationStart": { 1695 | "column": 28, 1696 | "line": 1, 1697 | }, 1698 | "rangeEnd": 58, 1699 | "rangeStart": 27, 1700 | }, 1701 | ] 1702 | `; 1703 | 1704 | exports[`simple interpolations standalone interpolation before comment (no semicolon, no space) 1`] = ` 1705 | [ 1706 | { 1707 | "css": "\${borderWidth}/* hello */", 1708 | "interpolationRanges": [ 1709 | { 1710 | "end": 41, 1711 | "start": 27, 1712 | }, 1713 | ], 1714 | "locationStart": { 1715 | "column": 28, 1716 | "line": 1, 1717 | }, 1718 | "rangeEnd": 52, 1719 | "rangeStart": 27, 1720 | }, 1721 | ] 1722 | `; 1723 | 1724 | exports[`simple interpolations standalone interpolation before comment (no semicolon, with space) 1`] = ` 1725 | [ 1726 | { 1727 | "css": "\${borderWidth} /* hello */", 1728 | "interpolationRanges": [ 1729 | { 1730 | "end": 41, 1731 | "start": 27, 1732 | }, 1733 | ], 1734 | "locationStart": { 1735 | "column": 28, 1736 | "line": 1, 1737 | }, 1738 | "rangeEnd": 53, 1739 | "rangeStart": 27, 1740 | }, 1741 | ] 1742 | `; 1743 | 1744 | exports[`simple interpolations standalone interpolation before comment (with semicolon and space) 1`] = ` 1745 | [ 1746 | { 1747 | "css": "\${borderWidth}; /* hello */", 1748 | "interpolationRanges": [ 1749 | { 1750 | "end": 41, 1751 | "start": 27, 1752 | }, 1753 | ], 1754 | "locationStart": { 1755 | "column": 28, 1756 | "line": 1, 1757 | }, 1758 | "rangeEnd": 54, 1759 | "rangeStart": 27, 1760 | }, 1761 | ] 1762 | `; 1763 | 1764 | exports[`simple interpolations standalone interpolation before comment (with semicolon, no space) 1`] = ` 1765 | [ 1766 | { 1767 | "css": "\${borderWidth};/* hello */", 1768 | "interpolationRanges": [ 1769 | { 1770 | "end": 41, 1771 | "start": 27, 1772 | }, 1773 | ], 1774 | "locationStart": { 1775 | "column": 28, 1776 | "line": 1, 1777 | }, 1778 | "rangeEnd": 53, 1779 | "rangeStart": 27, 1780 | }, 1781 | ] 1782 | `; 1783 | 1784 | exports[`simple interpolations standalone interpolation before comment. comment with a backslash 1`] = ` 1785 | [ 1786 | { 1787 | "css": "\${borderWidth};/* comment with a backslash \\ */", 1788 | "interpolationRanges": [ 1789 | { 1790 | "end": 41, 1791 | "start": 27, 1792 | }, 1793 | ], 1794 | "locationStart": { 1795 | "column": 28, 1796 | "line": 1, 1797 | }, 1798 | "rangeEnd": 74, 1799 | "rangeStart": 27, 1800 | }, 1801 | ] 1802 | `; 1803 | 1804 | exports[`simple interpolations standalone interpolation before declaration (no semicolon, no space) 1`] = ` 1805 | [ 1806 | { 1807 | "css": "\${borderWidth}color: red", 1808 | "interpolationRanges": [ 1809 | { 1810 | "end": 41, 1811 | "start": 27, 1812 | }, 1813 | ], 1814 | "locationStart": { 1815 | "column": 28, 1816 | "line": 1, 1817 | }, 1818 | "rangeEnd": 51, 1819 | "rangeStart": 27, 1820 | }, 1821 | ] 1822 | `; 1823 | 1824 | exports[`simple interpolations standalone interpolation before declaration (with semicolon and space) 1`] = ` 1825 | [ 1826 | { 1827 | "css": "\${borderWidth}; color: red", 1828 | "interpolationRanges": [ 1829 | { 1830 | "end": 41, 1831 | "start": 27, 1832 | }, 1833 | ], 1834 | "locationStart": { 1835 | "column": 28, 1836 | "line": 1, 1837 | }, 1838 | "rangeEnd": 53, 1839 | "rangeStart": 27, 1840 | }, 1841 | ] 1842 | `; 1843 | 1844 | exports[`simple interpolations standalone interpolation before declaration (with semicolon) 1`] = ` 1845 | [ 1846 | { 1847 | "css": "\${borderWidth};color: red", 1848 | "interpolationRanges": [ 1849 | { 1850 | "end": 41, 1851 | "start": 27, 1852 | }, 1853 | ], 1854 | "locationStart": { 1855 | "column": 28, 1856 | "line": 1, 1857 | }, 1858 | "rangeEnd": 52, 1859 | "rangeStart": 27, 1860 | }, 1861 | ] 1862 | `; 1863 | 1864 | exports[`simple interpolations standalone interpolation before declaration (with space) 1`] = ` 1865 | [ 1866 | { 1867 | "css": "\${borderWidth} color: red", 1868 | "interpolationRanges": [ 1869 | { 1870 | "end": 41, 1871 | "start": 27, 1872 | }, 1873 | ], 1874 | "locationStart": { 1875 | "column": 28, 1876 | "line": 1, 1877 | }, 1878 | "rangeEnd": 52, 1879 | "rangeStart": 27, 1880 | }, 1881 | ] 1882 | `; 1883 | 1884 | exports[`simple interpolations standalone interpolation between three declarations (no semicolon) 1`] = ` 1885 | [ 1886 | { 1887 | "css": "color: red; \${borderWidth} border-color: blue; \${anotherThing} display: none;", 1888 | "interpolationRanges": [ 1889 | { 1890 | "end": 53, 1891 | "start": 39, 1892 | }, 1893 | { 1894 | "end": 89, 1895 | "start": 74, 1896 | }, 1897 | ], 1898 | "locationStart": { 1899 | "column": 28, 1900 | "line": 1, 1901 | }, 1902 | "rangeEnd": 104, 1903 | "rangeStart": 27, 1904 | }, 1905 | ] 1906 | `; 1907 | 1908 | exports[`simple interpolations standalone interpolation between two declarations (no semicolon) 1`] = ` 1909 | [ 1910 | { 1911 | "css": "color: red; \${borderWidth} border-color: blue;", 1912 | "interpolationRanges": [ 1913 | { 1914 | "end": 53, 1915 | "start": 39, 1916 | }, 1917 | ], 1918 | "locationStart": { 1919 | "column": 28, 1920 | "line": 1, 1921 | }, 1922 | "rangeEnd": 73, 1923 | "rangeStart": 27, 1924 | }, 1925 | ] 1926 | `; 1927 | 1928 | exports[`simple interpolations standalone interpolation between two declarations (with semicolon) 1`] = ` 1929 | [ 1930 | { 1931 | "css": "color: red; \${borderWidth}; border-color: blue;", 1932 | "interpolationRanges": [ 1933 | { 1934 | "end": 53, 1935 | "start": 39, 1936 | }, 1937 | ], 1938 | "locationStart": { 1939 | "column": 28, 1940 | "line": 1, 1941 | }, 1942 | "rangeEnd": 74, 1943 | "rangeStart": 27, 1944 | }, 1945 | ] 1946 | `; 1947 | 1948 | exports[`simple interpolations standalone interpolation between two declarations, and also trailing interpolation 1`] = ` 1949 | [ 1950 | { 1951 | "css": "color: red; \${ borderWidth} border-color: blue; \${anotherThing}", 1952 | "interpolationRanges": [ 1953 | { 1954 | "end": 54, 1955 | "start": 39, 1956 | }, 1957 | { 1958 | "end": 90, 1959 | "start": 75, 1960 | }, 1961 | ], 1962 | "locationStart": { 1963 | "column": 28, 1964 | "line": 1, 1965 | }, 1966 | "rangeEnd": 90, 1967 | "rangeStart": 27, 1968 | }, 1969 | ] 1970 | `; 1971 | 1972 | exports[`simple interpolations standalone interpolation multiple after declaration (no semicolon, space after) 1`] = ` 1973 | [ 1974 | { 1975 | "css": "color: red; \${borderWidth} \${hello} ", 1976 | "interpolationRanges": [ 1977 | { 1978 | "end": 53, 1979 | "start": 39, 1980 | }, 1981 | { 1982 | "end": 62, 1983 | "start": 54, 1984 | }, 1985 | ], 1986 | "locationStart": { 1987 | "column": 28, 1988 | "line": 1, 1989 | }, 1990 | "rangeEnd": 63, 1991 | "rangeStart": 27, 1992 | }, 1993 | ] 1994 | `; 1995 | 1996 | exports[`supports TypeScript 1`] = ` 1997 | [ 1998 | { 1999 | "css": "color: red;", 2000 | "interpolationRanges": [], 2001 | "locationStart": { 2002 | "column": 55, 2003 | "line": 1, 2004 | }, 2005 | "rangeEnd": 65, 2006 | "rangeStart": 54, 2007 | }, 2008 | ] 2009 | `; 2010 | -------------------------------------------------------------------------------- /lib/__tests__/parse.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const parse = require('../parse'); 3 | 4 | describe('no interpolations', () => { 5 | test('one component', () => { 6 | let document = parse('let Component = styled.div`color: red;`;'); 7 | 8 | expect(document.nodes).toHaveLength(1); 9 | 10 | let firstComponent = document.first; 11 | 12 | expect(firstComponent.nodes).toHaveLength(1); 13 | expect(firstComponent.raws).toEqual({ 14 | isRuleLike: true, 15 | styledSyntaxIsComponent: true, 16 | styledSyntaxRangeStart: 27, 17 | styledSyntaxRangeEnd: 38, 18 | codeBefore: 'let Component = styled.div`', 19 | codeAfter: '`;', 20 | after: '', 21 | semicolon: true, 22 | }); 23 | expect(firstComponent.source.start).toEqual({ 24 | offset: 27, 25 | line: 1, 26 | column: 28, 27 | }); 28 | 29 | let decl = firstComponent.first; 30 | 31 | expect(decl.prop).toBe('color'); 32 | expect(decl.value).toBe('red'); 33 | expect(decl.source.start).toEqual({ 34 | offset: 27, 35 | line: 1, 36 | column: 28, 37 | }); 38 | expect(decl.source.end).toEqual({ 39 | offset: 37, 40 | line: 1, 41 | column: 38, 42 | }); 43 | expect(decl.rangeBy({})).toEqual({ 44 | start: { 45 | column: 28, 46 | line: 1, 47 | }, 48 | end: { 49 | column: 39, 50 | line: 1, 51 | }, 52 | }); 53 | expect(decl.rangeBy({ word: 'red' })).toEqual({ 54 | start: { 55 | column: 35, 56 | line: 1, 57 | }, 58 | end: { 59 | column: 38, 60 | line: 1, 61 | }, 62 | }); 63 | }); 64 | 65 | test('two components', () => { 66 | let document = parse( 67 | 'let Component = styled.div`color: red;`;\nlet Component = styled.div`border-color: blue`;', 68 | ); 69 | 70 | expect(document.nodes).toHaveLength(2); 71 | 72 | let firstComponent = document.first; 73 | 74 | expect(firstComponent.nodes).toHaveLength(1); 75 | expect(firstComponent.raws).toEqual({ 76 | isRuleLike: true, 77 | styledSyntaxIsComponent: true, 78 | styledSyntaxRangeStart: 27, 79 | styledSyntaxRangeEnd: 38, 80 | codeBefore: 'let Component = styled.div`', 81 | after: '', 82 | semicolon: true, 83 | }); 84 | expect(firstComponent.source.start).toEqual({ 85 | offset: 27, 86 | line: 1, 87 | column: 28, 88 | }); 89 | 90 | expect(firstComponent.first.prop).toBe('color'); 91 | expect(firstComponent.first.value).toBe('red'); 92 | expect(firstComponent.first.source.start).toEqual({ 93 | offset: 27, 94 | line: 1, 95 | column: 28, 96 | }); 97 | expect(firstComponent.first.source.end).toEqual({ 98 | offset: 37, 99 | line: 1, 100 | column: 38, 101 | }); 102 | 103 | let secondComponent = document.first.next(); 104 | 105 | expect(secondComponent.nodes).toHaveLength(1); 106 | expect(secondComponent.raws).toEqual({ 107 | isRuleLike: true, 108 | styledSyntaxIsComponent: true, 109 | styledSyntaxRangeStart: 68, 110 | styledSyntaxRangeEnd: 86, 111 | codeBefore: '`;\nlet Component = styled.div`', 112 | codeAfter: '`;', 113 | after: '', 114 | semicolon: false, 115 | }); 116 | expect(secondComponent.source.start).toEqual({ 117 | offset: 68, 118 | line: 2, 119 | column: 28, 120 | }); 121 | 122 | expect(secondComponent.first.prop).toBe('border-color'); 123 | expect(secondComponent.first.value).toBe('blue'); 124 | expect(secondComponent.first.source.start).toEqual({ 125 | offset: 68, 126 | line: 2, 127 | column: 28, 128 | }); 129 | expect(secondComponent.first.source.end).toEqual({ 130 | offset: 85, 131 | line: 2, 132 | column: 45, 133 | }); 134 | expect(secondComponent.rangeBy({})).toEqual({ 135 | start: { 136 | column: 28, 137 | line: 2, 138 | }, 139 | end: { 140 | column: 47, 141 | line: 2, 142 | }, 143 | }); 144 | expect(secondComponent.rangeBy({ word: 'blue' })).toEqual({ 145 | start: { 146 | column: 42, 147 | line: 2, 148 | }, 149 | end: { 150 | column: 46, 151 | line: 2, 152 | }, 153 | }); 154 | }); 155 | 156 | test('empty component', () => { 157 | let document = parse('let Component = styled.div``;'); 158 | 159 | expect(document.nodes).toHaveLength(1); 160 | expect(document.first.nodes).toHaveLength(0); 161 | expect(document.first.raws).toEqual({ 162 | isRuleLike: true, 163 | styledSyntaxIsComponent: true, 164 | styledSyntaxRangeStart: 27, 165 | styledSyntaxRangeEnd: 27, 166 | codeBefore: 'let Component = styled.div`', 167 | codeAfter: '`;', 168 | after: '', 169 | }); 170 | expect(document.first.toString()).toBe(''); 171 | }); 172 | 173 | test('property on its row. no extra space', () => { 174 | let document = parse('let Component = styled.div`\n\tcolor: red;\n`;'); 175 | 176 | expect(document.nodes).toHaveLength(1); 177 | 178 | let firstComponent = document.first; 179 | 180 | expect(firstComponent.nodes).toHaveLength(1); 181 | expect(firstComponent.raws).toEqual({ 182 | codeBefore: 'let Component = styled.div`', 183 | codeAfter: '`;', 184 | after: '\n', 185 | semicolon: true, 186 | isRuleLike: true, 187 | styledSyntaxIsComponent: true, 188 | styledSyntaxRangeEnd: 41, 189 | styledSyntaxRangeStart: 27, 190 | }); 191 | expect(firstComponent.source.start).toEqual({ 192 | offset: 27, 193 | line: 1, 194 | column: 28, 195 | }); 196 | 197 | let decl = firstComponent.first; 198 | 199 | expect(decl.prop).toBe('color'); 200 | expect(decl.value).toBe('red'); 201 | expect(decl.source.start).toEqual({ 202 | offset: 29, 203 | line: 2, 204 | column: 2, 205 | }); 206 | expect(decl.source.end).toEqual({ 207 | offset: 39, 208 | line: 2, 209 | column: 12, 210 | }); 211 | }); 212 | 213 | test('property on its row. extra space in the begining', () => { 214 | let document = parse('let Component = styled.div` \n\tcolor: red;\n`;'); 215 | 216 | expect(document.nodes).toHaveLength(1); 217 | expect(document.first.nodes).toHaveLength(1); 218 | expect(document.first.first.prop).toBe('color'); 219 | expect(document.first.first.value).toBe('red'); 220 | expect(document.first.raws).toEqual({ 221 | codeBefore: 'let Component = styled.div`', 222 | codeAfter: '`;', 223 | after: '\n', 224 | semicolon: true, 225 | isRuleLike: true, 226 | styledSyntaxIsComponent: true, 227 | styledSyntaxRangeEnd: 42, 228 | styledSyntaxRangeStart: 27, 229 | }); 230 | 231 | expect(document.first.toString()).toBe(' \n\tcolor: red;\n'); 232 | }); 233 | 234 | test('empty file', () => { 235 | let document = parse(''); 236 | 237 | expect(document.nodes).toHaveLength(0); 238 | expect(document.toString()).toBe(''); 239 | }); 240 | 241 | test('no components in a file', () => { 242 | let document = parse('function styled() { return false }'); 243 | 244 | expect(document.nodes).toHaveLength(0); 245 | expect(document.toString()).toBe(''); 246 | expect(document.raws).toEqual({}); 247 | }); 248 | 249 | test('selector could be on multiple lines', () => { 250 | let document = parse('let Component = styled.div`a,\n\tb { color: red; }`;'); 251 | 252 | expect(document.nodes).toHaveLength(1); 253 | expect(document.first.nodes).toHaveLength(1); 254 | expect(document.first.raws).toEqual({ 255 | isRuleLike: true, 256 | styledSyntaxIsComponent: true, 257 | styledSyntaxRangeStart: 27, 258 | styledSyntaxRangeEnd: 48, 259 | codeBefore: 'let Component = styled.div`', 260 | codeAfter: '`;', 261 | after: '', 262 | semicolon: false, 263 | }); 264 | 265 | let firstNode = document.first.first; 266 | 267 | expect(firstNode.type).toBe('rule'); 268 | expect(firstNode.selector).toBe('a,\n\tb'); 269 | expect(firstNode.nodes).toHaveLength(1); 270 | expect(firstNode.raws.before).toBe(''); 271 | }); 272 | 273 | test('comment in selector', () => { 274 | let document = parse('let Component = styled.div`a, /* hello */ b { color: red; }`;'); 275 | 276 | expect(document.nodes).toHaveLength(1); 277 | expect(document.first.nodes).toHaveLength(1); 278 | expect(document.first.raws).toEqual({ 279 | isRuleLike: true, 280 | styledSyntaxIsComponent: true, 281 | styledSyntaxRangeStart: 27, 282 | styledSyntaxRangeEnd: 59, 283 | codeBefore: 'let Component = styled.div`', 284 | codeAfter: '`;', 285 | after: '', 286 | semicolon: false, 287 | }); 288 | 289 | let firstNode = document.first.first; 290 | 291 | expect(firstNode.type).toBe('rule'); 292 | expect(firstNode.selector).toBe('a, b'); 293 | expect(firstNode.nodes).toHaveLength(1); 294 | expect(firstNode.raws.before).toBe(''); 295 | expect(firstNode.raws.selector).toEqual({ 296 | value: 'a, b', 297 | raw: 'a, /* hello */ b', 298 | }); 299 | }); 300 | }); 301 | 302 | describe('simple interpolations', () => { 303 | describe('properties', () => { 304 | test('property value (no semicolon)', () => { 305 | let document = parse('let Component = styled.div`color: ${red}`;'); 306 | 307 | expect(document.nodes).toHaveLength(1); 308 | expect(document.first.nodes).toHaveLength(1); 309 | expect(document.first.raws).toEqual({ 310 | isRuleLike: true, 311 | styledSyntaxIsComponent: true, 312 | styledSyntaxRangeStart: 27, 313 | styledSyntaxRangeEnd: 40, 314 | codeBefore: 'let Component = styled.div`', 315 | codeAfter: '`;', 316 | after: '', 317 | semicolon: false, 318 | }); 319 | 320 | let firstNode = document.first.first; 321 | 322 | expect(firstNode.prop).toBe('color'); 323 | expect(firstNode.value).toBe('${red}'); 324 | }); 325 | 326 | test('property value with symbol right before interpolation', () => { 327 | let document = parse('let Component = styled.div`margin: -${space}`;'); 328 | 329 | expect(document.nodes).toHaveLength(1); 330 | expect(document.first.nodes).toHaveLength(1); 331 | expect(document.first.raws).toEqual({ 332 | isRuleLike: true, 333 | styledSyntaxIsComponent: true, 334 | styledSyntaxRangeStart: 27, 335 | styledSyntaxRangeEnd: 44, 336 | codeBefore: 'let Component = styled.div`', 337 | codeAfter: '`;', 338 | after: '', 339 | semicolon: false, 340 | }); 341 | 342 | let firstNode = document.first.first; 343 | 344 | expect(firstNode.prop).toBe('margin'); 345 | expect(firstNode.value).toBe('-${space}'); 346 | }); 347 | 348 | test('property value (with semicolon)', () => { 349 | let document = parse('let Component = styled.div`color: ${red};`;'); 350 | 351 | expect(document.nodes).toHaveLength(1); 352 | expect(document.first.nodes).toHaveLength(1); 353 | expect(document.first.raws).toEqual({ 354 | isRuleLike: true, 355 | styledSyntaxIsComponent: true, 356 | styledSyntaxRangeStart: 27, 357 | styledSyntaxRangeEnd: 41, 358 | codeBefore: 'let Component = styled.div`', 359 | codeAfter: '`;', 360 | after: '', 361 | semicolon: true, 362 | }); 363 | 364 | let firstNode = document.first.first; 365 | 366 | expect(firstNode.prop).toBe('color'); 367 | expect(firstNode.value).toBe('${red}'); 368 | }); 369 | 370 | test('property value and !important (no semicolon)', () => { 371 | let document = parse('let Component = styled.div`color: ${red} !important`;'); 372 | 373 | expect(document.nodes).toHaveLength(1); 374 | expect(document.first.nodes).toHaveLength(1); 375 | expect(document.first.raws).toEqual({ 376 | isRuleLike: true, 377 | styledSyntaxIsComponent: true, 378 | styledSyntaxRangeStart: 27, 379 | styledSyntaxRangeEnd: 51, 380 | codeBefore: 'let Component = styled.div`', 381 | codeAfter: '`;', 382 | after: '', 383 | semicolon: false, 384 | }); 385 | 386 | let firstNode = document.first.first; 387 | 388 | expect(firstNode.prop).toBe('color'); 389 | expect(firstNode.value).toBe('${red}'); 390 | expect(firstNode.important).toBe(true); 391 | }); 392 | 393 | test('property value and !important (with semicolon)', () => { 394 | let document = parse('let Component = styled.div`color: ${red} !important;`;'); 395 | 396 | expect(document.nodes).toHaveLength(1); 397 | expect(document.first.nodes).toHaveLength(1); 398 | expect(document.first.raws).toEqual({ 399 | isRuleLike: true, 400 | styledSyntaxIsComponent: true, 401 | styledSyntaxRangeStart: 27, 402 | styledSyntaxRangeEnd: 52, 403 | codeBefore: 'let Component = styled.div`', 404 | codeAfter: '`;', 405 | after: '', 406 | semicolon: true, 407 | }); 408 | 409 | let firstNode = document.first.first; 410 | 411 | expect(firstNode.prop).toBe('color'); 412 | expect(firstNode.value).toBe('${red}'); 413 | expect(firstNode.important).toBe(true); 414 | }); 415 | 416 | test('property value with two interpolations', () => { 417 | let document = parse( 418 | 'let Component = styled.div`box-shadow: ${elevation1}, ${elevation2}`;', 419 | ); 420 | 421 | expect(document.nodes).toHaveLength(1); 422 | expect(document.first.nodes).toHaveLength(1); 423 | expect(document.first.raws).toEqual({ 424 | isRuleLike: true, 425 | styledSyntaxIsComponent: true, 426 | styledSyntaxRangeStart: 27, 427 | styledSyntaxRangeEnd: 67, 428 | codeBefore: 'let Component = styled.div`', 429 | codeAfter: '`;', 430 | after: '', 431 | semicolon: false, 432 | }); 433 | 434 | let firstNode = document.first.first; 435 | 436 | expect(firstNode.prop).toBe('box-shadow'); 437 | expect(firstNode.value).toBe('${elevation1}, ${elevation2}'); 438 | }); 439 | 440 | test('property value with props interpolation', () => { 441 | let document = parse( 442 | 'let Component = styled.div`background-image: ${(props) => props.backgroundImage}`;', 443 | ); 444 | 445 | expect(document.nodes).toHaveLength(1); 446 | expect(document.first.nodes).toHaveLength(1); 447 | expect(document.first.raws).toEqual({ 448 | isRuleLike: true, 449 | styledSyntaxIsComponent: true, 450 | styledSyntaxRangeStart: 27, 451 | styledSyntaxRangeEnd: 80, 452 | codeBefore: 'let Component = styled.div`', 453 | codeAfter: '`;', 454 | after: '', 455 | semicolon: false, 456 | }); 457 | 458 | let firstNode = document.first.first; 459 | 460 | expect(firstNode.prop).toBe('background-image'); 461 | expect(firstNode.value).toBe('${(props) => props.backgroundImage}'); 462 | }); 463 | 464 | test('property value with interpolation inside url function', () => { 465 | let document = parse('let Component = styled.div`background-image: url(${imageUrl})`;'); 466 | 467 | expect(document.nodes).toHaveLength(1); 468 | expect(document.first.nodes).toHaveLength(1); 469 | expect(document.first.raws).toEqual({ 470 | isRuleLike: true, 471 | styledSyntaxIsComponent: true, 472 | styledSyntaxRangeStart: 27, 473 | styledSyntaxRangeEnd: 61, 474 | codeBefore: 'let Component = styled.div`', 475 | codeAfter: '`;', 476 | after: '', 477 | semicolon: false, 478 | }); 479 | 480 | let firstNode = document.first.first; 481 | 482 | expect(firstNode.prop).toBe('background-image'); 483 | expect(firstNode.value).toBe('url(${imageUrl})'); 484 | }); 485 | 486 | test('property value with props interpolation inside url function', () => { 487 | let document = parse( 488 | 'let Component = styled.div`background-image: url(${(props) => props.backgroundImage})`;', 489 | ); 490 | 491 | expect(document.nodes).toHaveLength(1); 492 | expect(document.first.nodes).toHaveLength(1); 493 | expect(document.first.raws).toEqual({ 494 | isRuleLike: true, 495 | styledSyntaxIsComponent: true, 496 | styledSyntaxRangeStart: 27, 497 | styledSyntaxRangeEnd: 85, 498 | codeBefore: 'let Component = styled.div`', 499 | codeAfter: '`;', 500 | after: '', 501 | semicolon: false, 502 | }); 503 | 504 | let firstNode = document.first.first; 505 | 506 | expect(firstNode.prop).toBe('background-image'); 507 | expect(firstNode.value).toBe('url(${(props) => props.backgroundImage})'); 508 | }); 509 | 510 | test('property name (first property)', () => { 511 | let document = parse('let Component = styled.div`${color}: red`;'); 512 | 513 | expect(document.nodes).toHaveLength(1); 514 | expect(document.first.nodes).toHaveLength(1); 515 | expect(document.first.raws).toEqual({ 516 | isRuleLike: true, 517 | styledSyntaxIsComponent: true, 518 | styledSyntaxRangeStart: 27, 519 | styledSyntaxRangeEnd: 40, 520 | codeBefore: 'let Component = styled.div`', 521 | codeAfter: '`;', 522 | after: '', 523 | semicolon: false, 524 | }); 525 | 526 | let firstNode = document.first.first; 527 | 528 | expect(firstNode.prop).toBe('${color}'); 529 | expect(firstNode.value).toBe('red'); 530 | }); 531 | 532 | test('property name (second property)', () => { 533 | let document = parse('let Component = styled.div`display: flex; ${color}: red`;'); 534 | 535 | expect(document.nodes).toHaveLength(1); 536 | expect(document.first.nodes).toHaveLength(2); 537 | expect(document.first.raws).toEqual({ 538 | isRuleLike: true, 539 | styledSyntaxIsComponent: true, 540 | styledSyntaxRangeStart: 27, 541 | styledSyntaxRangeEnd: 55, 542 | codeBefore: 'let Component = styled.div`', 543 | codeAfter: '`;', 544 | after: '', 545 | semicolon: false, 546 | }); 547 | 548 | let firstNode = document.first.first; 549 | let secondNode = firstNode.next(); 550 | 551 | expect(firstNode.prop).toBe('display'); 552 | expect(firstNode.value).toBe('flex'); 553 | 554 | expect(secondNode.prop).toBe('${color}'); 555 | expect(secondNode.value).toBe('red'); 556 | }); 557 | }); 558 | 559 | describe('selectors', () => { 560 | test('in selector (whole selector)', () => { 561 | let document = parse('let Component = styled.div`${Component} { color: red; }`;'); 562 | 563 | expect(document.nodes).toHaveLength(1); 564 | expect(document.first.nodes).toHaveLength(1); 565 | expect(document.first.raws).toEqual({ 566 | isRuleLike: true, 567 | styledSyntaxIsComponent: true, 568 | styledSyntaxRangeStart: 27, 569 | styledSyntaxRangeEnd: 55, 570 | codeBefore: 'let Component = styled.div`', 571 | codeAfter: '`;', 572 | after: '', 573 | semicolon: false, 574 | }); 575 | 576 | let firstNode = document.first.first; 577 | 578 | expect(firstNode.type).toBe('rule'); 579 | expect(firstNode.selector).toBe('${Component}'); 580 | expect(firstNode.nodes).toHaveLength(1); 581 | }); 582 | 583 | test('two interpolations in selector (whole selector)', () => { 584 | let document = parse( 585 | 'let Component = styled.div`${Component} ${Component1} { color: red; }`;', 586 | ); 587 | 588 | expect(document.nodes).toHaveLength(1); 589 | expect(document.first.nodes).toHaveLength(1); 590 | expect(document.first.raws).toEqual({ 591 | isRuleLike: true, 592 | styledSyntaxIsComponent: true, 593 | styledSyntaxRangeStart: 27, 594 | styledSyntaxRangeEnd: 69, 595 | codeBefore: 'let Component = styled.div`', 596 | codeAfter: '`;', 597 | after: '', 598 | semicolon: false, 599 | }); 600 | 601 | let firstNode = document.first.first; 602 | 603 | expect(firstNode.type).toBe('rule'); 604 | expect(firstNode.selector).toBe('${Component} ${Component1}'); 605 | expect(firstNode.nodes).toHaveLength(1); 606 | }); 607 | 608 | test('in selector (whole selector starts with a dot)', () => { 609 | let document = parse('let Component = styled.div`.${Component} { color: red; }`;'); 610 | 611 | expect(document.nodes).toHaveLength(1); 612 | expect(document.first.nodes).toHaveLength(1); 613 | expect(document.first.raws).toEqual({ 614 | isRuleLike: true, 615 | styledSyntaxIsComponent: true, 616 | styledSyntaxRangeStart: 27, 617 | styledSyntaxRangeEnd: 56, 618 | codeBefore: 'let Component = styled.div`', 619 | codeAfter: '`;', 620 | after: '', 621 | semicolon: false, 622 | }); 623 | 624 | let firstNode = document.first.first; 625 | 626 | expect(firstNode.type).toBe('rule'); 627 | expect(firstNode.selector).toBe('.${Component}'); 628 | expect(firstNode.nodes).toHaveLength(1); 629 | }); 630 | 631 | test('in selector (first selector is interpolation)', () => { 632 | let document = parse('let Component = styled.div`${Component}, a { color: red; }`;'); 633 | 634 | expect(document.nodes).toHaveLength(1); 635 | expect(document.first.nodes).toHaveLength(1); 636 | expect(document.first.raws).toEqual({ 637 | isRuleLike: true, 638 | styledSyntaxIsComponent: true, 639 | styledSyntaxRangeStart: 27, 640 | styledSyntaxRangeEnd: 58, 641 | codeBefore: 'let Component = styled.div`', 642 | codeAfter: '`;', 643 | after: '', 644 | semicolon: false, 645 | }); 646 | 647 | let firstNode = document.first.first; 648 | 649 | expect(firstNode.type).toBe('rule'); 650 | expect(firstNode.selector).toBe('${Component}, a'); 651 | expect(firstNode.nodes).toHaveLength(1); 652 | }); 653 | 654 | test('in selector (second selector is interpolation, two selectors)', () => { 655 | let document = parse('let Component = styled.div`a, ${Component} { color: red; }`;'); 656 | 657 | expect(document.nodes).toHaveLength(1); 658 | expect(document.first.nodes).toHaveLength(1); 659 | expect(document.first.raws).toEqual({ 660 | isRuleLike: true, 661 | styledSyntaxIsComponent: true, 662 | styledSyntaxRangeStart: 27, 663 | styledSyntaxRangeEnd: 58, 664 | codeBefore: 'let Component = styled.div`', 665 | codeAfter: '`;', 666 | after: '', 667 | semicolon: false, 668 | }); 669 | 670 | let firstNode = document.first.first; 671 | 672 | expect(firstNode.type).toBe('rule'); 673 | expect(firstNode.selector).toBe('a, ${Component}'); 674 | expect(firstNode.nodes).toHaveLength(1); 675 | }); 676 | 677 | test('in selector (second part is interpolation, one selector)', () => { 678 | let document = parse('let Component = styled.div`a ${Component} { color: red; }`;'); 679 | 680 | expect(document.nodes).toHaveLength(1); 681 | expect(document.first.nodes).toHaveLength(1); 682 | expect(document.first.raws).toEqual({ 683 | isRuleLike: true, 684 | styledSyntaxIsComponent: true, 685 | styledSyntaxRangeStart: 27, 686 | styledSyntaxRangeEnd: 57, 687 | codeBefore: 'let Component = styled.div`', 688 | codeAfter: '`;', 689 | after: '', 690 | semicolon: false, 691 | }); 692 | 693 | let firstNode = document.first.first; 694 | 695 | expect(firstNode.type).toBe('rule'); 696 | expect(firstNode.selector).toBe('a ${Component}'); 697 | expect(firstNode.nodes).toHaveLength(1); 698 | }); 699 | 700 | test('in selector (first part is interpolation, one selector)', () => { 701 | let document = parse('let Component = styled.div`${Component} a { color: red; }`;'); 702 | 703 | expect(document.nodes).toHaveLength(1); 704 | expect(document.first.nodes).toHaveLength(1); 705 | expect(document.first.raws).toEqual({ 706 | isRuleLike: true, 707 | styledSyntaxIsComponent: true, 708 | styledSyntaxRangeStart: 27, 709 | styledSyntaxRangeEnd: 57, 710 | codeBefore: 'let Component = styled.div`', 711 | codeAfter: '`;', 712 | after: '', 713 | semicolon: false, 714 | }); 715 | 716 | let firstNode = document.first.first; 717 | 718 | expect(firstNode.type).toBe('rule'); 719 | expect(firstNode.selector).toBe('${Component} a'); 720 | expect(firstNode.nodes).toHaveLength(1); 721 | }); 722 | 723 | test('interpolation with semicolon before selector', () => { 724 | let document = parse('let Component = styled.div`${Component}; a { color: red; }`;'); 725 | 726 | expect(document.nodes).toHaveLength(1); 727 | expect(document.first.nodes).toHaveLength(1); 728 | expect(document.first.raws).toEqual({ 729 | isRuleLike: true, 730 | styledSyntaxIsComponent: true, 731 | styledSyntaxRangeStart: 27, 732 | styledSyntaxRangeEnd: 58, 733 | codeBefore: 'let Component = styled.div`', 734 | codeAfter: '`;', 735 | after: '', 736 | semicolon: false, 737 | }); 738 | 739 | let firstNode = document.first.first; 740 | 741 | expect(firstNode.type).toBe('rule'); 742 | expect(firstNode.selector).toBe('a'); 743 | expect(firstNode.nodes).toHaveLength(1); 744 | expect(firstNode.raws.before).toBe('${Component}; '); 745 | }); 746 | 747 | test('interpolation on a new line before selector', () => { 748 | let document = parse('let Component = styled.div`${hello}\n\ta { color: red; }`;'); 749 | 750 | expect(document.nodes).toHaveLength(1); 751 | expect(document.first.nodes).toHaveLength(1); 752 | expect(document.first.raws).toEqual({ 753 | isRuleLike: true, 754 | styledSyntaxIsComponent: true, 755 | styledSyntaxRangeStart: 27, 756 | styledSyntaxRangeEnd: 54, 757 | codeBefore: 'let Component = styled.div`', 758 | codeAfter: '`;', 759 | after: '', 760 | semicolon: false, 761 | }); 762 | 763 | let firstNode = document.first.first; 764 | 765 | expect(firstNode.type).toBe('rule'); 766 | expect(firstNode.selector).toBe('a'); 767 | expect(firstNode.nodes).toHaveLength(1); 768 | expect(firstNode.raws.before).toBe('${hello}\n\t'); 769 | }); 770 | 771 | test('two interpolations on a new line before selector', () => { 772 | let document = parse( 773 | 'let Component = styled.div`${hello}\n\t${hello}\n\ta { color: red; }`;', 774 | ); 775 | 776 | expect(document.nodes).toHaveLength(1); 777 | expect(document.first.nodes).toHaveLength(1); 778 | expect(document.first.raws).toEqual({ 779 | isRuleLike: true, 780 | styledSyntaxIsComponent: true, 781 | styledSyntaxRangeStart: 27, 782 | styledSyntaxRangeEnd: 64, 783 | codeBefore: 'let Component = styled.div`', 784 | codeAfter: '`;', 785 | after: '', 786 | semicolon: false, 787 | }); 788 | 789 | let firstNode = document.first.first; 790 | 791 | expect(firstNode.type).toBe('rule'); 792 | expect(firstNode.selector).toBe('a'); 793 | expect(firstNode.nodes).toHaveLength(1); 794 | expect(firstNode.raws.before).toBe('${hello}\n\t${hello}\n\t'); 795 | }); 796 | 797 | test('comment in selector with interpolation', () => { 798 | let document = parse( 799 | 'let Component = styled.div`${Card}:hover, /* hello */ b { color: red; }`;', 800 | ); 801 | 802 | expect(document.nodes).toHaveLength(1); 803 | expect(document.first.nodes).toHaveLength(1); 804 | expect(document.first.raws).toEqual({ 805 | isRuleLike: true, 806 | styledSyntaxIsComponent: true, 807 | styledSyntaxRangeStart: 27, 808 | styledSyntaxRangeEnd: 71, 809 | codeBefore: 'let Component = styled.div`', 810 | codeAfter: '`;', 811 | after: '', 812 | semicolon: false, 813 | }); 814 | 815 | let firstNode = document.first.first; 816 | 817 | expect(firstNode.type).toBe('rule'); 818 | expect(firstNode.selector).toBe('${Card}:hover, b'); 819 | expect(firstNode.nodes).toHaveLength(1); 820 | expect(firstNode.raws.before).toBe(''); 821 | expect(firstNode.raws.selector).toEqual({ 822 | value: '${Card}:hover, b', 823 | raw: '${Card}:hover, /* hello */ b', 824 | }); 825 | }); 826 | }); 827 | 828 | describe('standalone interpolation', () => { 829 | test('after declaration (no semicolon, no space after)', () => { 830 | let document = parse('let Component = styled.div`color: red; ${borderWidth}`;'); 831 | 832 | expect(document.nodes).toHaveLength(1); 833 | expect(document.first.nodes).toHaveLength(1); 834 | expect(document.first.raws).toEqual({ 835 | isRuleLike: true, 836 | styledSyntaxIsComponent: true, 837 | styledSyntaxRangeStart: 27, 838 | styledSyntaxRangeEnd: 53, 839 | codeBefore: 'let Component = styled.div`', 840 | codeAfter: '`;', 841 | after: ' ${borderWidth}', 842 | semicolon: true, 843 | }); 844 | 845 | let firstNode = document.first.first; 846 | 847 | expect(firstNode.prop).toBe('color'); 848 | expect(firstNode.value).toBe('red'); 849 | }); 850 | 851 | test('after declaration (no semicolon, space after)', () => { 852 | let document = parse('let Component = styled.div`color: red; ${borderWidth} `;'); 853 | 854 | expect(document.nodes).toHaveLength(1); 855 | expect(document.first.nodes).toHaveLength(1); 856 | expect(document.first.raws).toEqual({ 857 | isRuleLike: true, 858 | styledSyntaxIsComponent: true, 859 | styledSyntaxRangeStart: 27, 860 | styledSyntaxRangeEnd: 54, 861 | codeBefore: 'let Component = styled.div`', 862 | codeAfter: '`;', 863 | after: ' ${borderWidth} ', 864 | semicolon: true, 865 | }); 866 | 867 | let firstNode = document.first.first; 868 | 869 | expect(firstNode.prop).toBe('color'); 870 | expect(firstNode.value).toBe('red'); 871 | }); 872 | 873 | test('multiple after declaration (no semicolon, space after)', () => { 874 | let document = parse( 875 | 'let Component = styled.div`color: red; ${borderWidth} ${hello} `;', 876 | ); 877 | 878 | expect(document.nodes).toHaveLength(1); 879 | expect(document.first.nodes).toHaveLength(1); 880 | expect(document.first.raws).toEqual({ 881 | isRuleLike: true, 882 | styledSyntaxIsComponent: true, 883 | styledSyntaxRangeStart: 27, 884 | styledSyntaxRangeEnd: 63, 885 | codeBefore: 'let Component = styled.div`', 886 | codeAfter: '`;', 887 | after: ' ${borderWidth} ${hello} ', 888 | semicolon: true, 889 | }); 890 | 891 | let firstNode = document.first.first; 892 | 893 | expect(firstNode.prop).toBe('color'); 894 | expect(firstNode.value).toBe('red'); 895 | }); 896 | 897 | test('after declaration (with semicolon)', () => { 898 | let document = parse('let Component = styled.div`color: red; ${borderWidth};`;'); 899 | 900 | expect(document.nodes).toHaveLength(1); 901 | expect(document.first.nodes).toHaveLength(1); 902 | expect(document.first.raws).toEqual({ 903 | isRuleLike: true, 904 | styledSyntaxIsComponent: true, 905 | styledSyntaxRangeStart: 27, 906 | styledSyntaxRangeEnd: 54, 907 | codeBefore: 'let Component = styled.div`', 908 | codeAfter: '`;', 909 | after: ' ${borderWidth};', 910 | semicolon: true, 911 | }); 912 | 913 | let firstNode = document.first.first; 914 | 915 | expect(firstNode.prop).toBe('color'); 916 | expect(firstNode.value).toBe('red'); 917 | }); 918 | 919 | test('throws for non-interpolation word after declaration', () => { 920 | expect(() => { 921 | parse('let Component = styled.div`color: red; ${borderWidth} notInterpolation`;'); 922 | }).toThrow(/:1:55: Unknown word/); 923 | }); 924 | 925 | test('before declaration (no semicolon, no space)', () => { 926 | let document = parse('let Component = styled.div`${borderWidth}color: red`;'); 927 | 928 | expect(document.nodes).toHaveLength(1); 929 | expect(document.first.nodes).toHaveLength(1); 930 | expect(document.first.raws).toEqual({ 931 | isRuleLike: true, 932 | styledSyntaxIsComponent: true, 933 | styledSyntaxRangeStart: 27, 934 | styledSyntaxRangeEnd: 51, 935 | codeBefore: 'let Component = styled.div`', 936 | codeAfter: '`;', 937 | after: '', 938 | semicolon: false, 939 | }); 940 | 941 | let firstNode = document.first.first; 942 | 943 | expect(firstNode.prop).toBe('${borderWidth}color'); 944 | expect(firstNode.value).toBe('red'); 945 | }); 946 | 947 | test('before declaration (with space)', () => { 948 | let document = parse('let Component = styled.div`${borderWidth} color: red`;'); 949 | 950 | expect(document.nodes).toHaveLength(1); 951 | expect(document.first.nodes).toHaveLength(1); 952 | expect(document.first.raws).toEqual({ 953 | isRuleLike: true, 954 | styledSyntaxIsComponent: true, 955 | styledSyntaxRangeStart: 27, 956 | styledSyntaxRangeEnd: 52, 957 | codeBefore: 'let Component = styled.div`', 958 | codeAfter: '`;', 959 | after: '', 960 | semicolon: false, 961 | }); 962 | 963 | let firstNode = document.first.first; 964 | 965 | expect(firstNode.prop).toBe('color'); 966 | expect(firstNode.value).toBe('red'); 967 | expect(firstNode.raws.before).toBe('${borderWidth} '); 968 | }); 969 | 970 | test('before declaration (with semicolon)', () => { 971 | let document = parse('let Component = styled.div`${borderWidth};color: red`;'); 972 | 973 | expect(document.nodes).toHaveLength(1); 974 | expect(document.first.nodes).toHaveLength(1); 975 | expect(document.first.raws).toEqual({ 976 | isRuleLike: true, 977 | styledSyntaxIsComponent: true, 978 | styledSyntaxRangeStart: 27, 979 | styledSyntaxRangeEnd: 52, 980 | codeBefore: 'let Component = styled.div`', 981 | codeAfter: '`;', 982 | after: '', 983 | semicolon: false, 984 | }); 985 | 986 | let firstNode = document.first.first; 987 | 988 | expect(firstNode.prop).toBe('color'); 989 | expect(firstNode.value).toBe('red'); 990 | expect(firstNode.raws.before).toBe('${borderWidth};'); 991 | }); 992 | 993 | test('before declaration (with semicolon and space)', () => { 994 | let document = parse('let Component = styled.div`${borderWidth}; color: red`;'); 995 | 996 | expect(document.nodes).toHaveLength(1); 997 | expect(document.first.nodes).toHaveLength(1); 998 | expect(document.first.raws).toEqual({ 999 | isRuleLike: true, 1000 | styledSyntaxIsComponent: true, 1001 | styledSyntaxRangeStart: 27, 1002 | styledSyntaxRangeEnd: 53, 1003 | codeBefore: 'let Component = styled.div`', 1004 | codeAfter: '`;', 1005 | after: '', 1006 | semicolon: false, 1007 | }); 1008 | 1009 | let firstNode = document.first.first; 1010 | 1011 | expect(firstNode.prop).toBe('color'); 1012 | expect(firstNode.value).toBe('red'); 1013 | expect(firstNode.raws.before).toBe('${borderWidth}; '); 1014 | }); 1015 | 1016 | test('between two declarations (no semicolon)', () => { 1017 | let document = parse( 1018 | 'let Component = styled.div`color: red; ${borderWidth} border-color: blue;`;', 1019 | ); 1020 | 1021 | expect(document.nodes).toHaveLength(1); 1022 | expect(document.first.nodes).toHaveLength(2); 1023 | expect(document.first.raws).toEqual({ 1024 | isRuleLike: true, 1025 | styledSyntaxIsComponent: true, 1026 | styledSyntaxRangeStart: 27, 1027 | styledSyntaxRangeEnd: 73, 1028 | codeBefore: 'let Component = styled.div`', 1029 | codeAfter: '`;', 1030 | after: '', 1031 | semicolon: true, 1032 | }); 1033 | 1034 | let firstNode = document.first.first; 1035 | let secondNode = firstNode.next(); 1036 | 1037 | expect(firstNode.prop).toBe('color'); 1038 | expect(firstNode.value).toBe('red'); 1039 | 1040 | expect(secondNode.prop).toBe('border-color'); 1041 | expect(secondNode.value).toBe('blue'); 1042 | 1043 | expect(secondNode.raws.before).toBe(' ${borderWidth} '); 1044 | }); 1045 | 1046 | test('between two declarations (with semicolon)', () => { 1047 | let document = parse( 1048 | 'let Component = styled.div`color: red; ${borderWidth}; border-color: blue;`;', 1049 | ); 1050 | 1051 | expect(document.nodes).toHaveLength(1); 1052 | expect(document.first.nodes).toHaveLength(2); 1053 | expect(document.first.raws).toEqual({ 1054 | isRuleLike: true, 1055 | styledSyntaxIsComponent: true, 1056 | styledSyntaxRangeStart: 27, 1057 | styledSyntaxRangeEnd: 74, 1058 | codeBefore: 'let Component = styled.div`', 1059 | codeAfter: '`;', 1060 | after: '', 1061 | semicolon: true, 1062 | }); 1063 | 1064 | let firstNode = document.first.first; 1065 | let secondNode = firstNode.next(); 1066 | 1067 | expect(firstNode.prop).toBe('color'); 1068 | expect(firstNode.value).toBe('red'); 1069 | 1070 | expect(secondNode.prop).toBe('border-color'); 1071 | expect(secondNode.value).toBe('blue'); 1072 | 1073 | expect(secondNode.raws.before).toBe(' ${borderWidth}; '); 1074 | }); 1075 | 1076 | test('as the only content (no semicolon)', () => { 1077 | let document = parse('let Component = styled.div`${color}`;'); 1078 | 1079 | expect(document.nodes).toHaveLength(1); 1080 | expect(document.first.nodes).toHaveLength(0); 1081 | expect(document.first.raws).toEqual({ 1082 | isRuleLike: true, 1083 | styledSyntaxIsComponent: true, 1084 | styledSyntaxRangeStart: 27, 1085 | styledSyntaxRangeEnd: 35, 1086 | codeBefore: 'let Component = styled.div`', 1087 | codeAfter: '`;', 1088 | after: '${color}', 1089 | }); 1090 | }); 1091 | 1092 | test('as the only content (with semicolon)', () => { 1093 | let document = parse('let Component = styled.div`${color};`;'); 1094 | 1095 | expect(document.nodes).toHaveLength(1); 1096 | expect(document.first.nodes).toHaveLength(0); 1097 | expect(document.first.raws).toEqual({ 1098 | isRuleLike: true, 1099 | styledSyntaxIsComponent: true, 1100 | styledSyntaxRangeStart: 27, 1101 | styledSyntaxRangeEnd: 36, 1102 | codeBefore: 'let Component = styled.div`', 1103 | codeAfter: '`;', 1104 | after: '${color};', 1105 | }); 1106 | }); 1107 | 1108 | test('between three declarations (no semicolon)', () => { 1109 | let document = parse( 1110 | 'let Component = styled.div`color: red; ${borderWidth} border-color: blue; ${anotherThing} display: none;`;', 1111 | ); 1112 | 1113 | expect(document.nodes).toHaveLength(1); 1114 | expect(document.first.nodes).toHaveLength(3); 1115 | expect(document.first.raws).toEqual({ 1116 | isRuleLike: true, 1117 | styledSyntaxIsComponent: true, 1118 | styledSyntaxRangeStart: 27, 1119 | styledSyntaxRangeEnd: 104, 1120 | codeBefore: 'let Component = styled.div`', 1121 | codeAfter: '`;', 1122 | after: '', 1123 | semicolon: true, 1124 | }); 1125 | 1126 | let firstNode = document.first.first; 1127 | let secondNode = firstNode.next(); 1128 | let thirdNode = secondNode.next(); 1129 | 1130 | expect(firstNode.prop).toBe('color'); 1131 | expect(firstNode.value).toBe('red'); 1132 | 1133 | expect(secondNode.prop).toBe('border-color'); 1134 | expect(secondNode.value).toBe('blue'); 1135 | 1136 | expect(secondNode.raws.before).toBe(' ${borderWidth} '); 1137 | 1138 | expect(thirdNode.prop).toBe('display'); 1139 | expect(thirdNode.value).toBe('none'); 1140 | 1141 | expect(thirdNode.raws.before).toBe(' ${anotherThing} '); 1142 | }); 1143 | 1144 | test('between two declarations, and also trailing interpolation', () => { 1145 | let document = parse( 1146 | 'let Component = styled.div`color: red; ${ borderWidth} border-color: blue; ${anotherThing}`;', 1147 | ); 1148 | 1149 | expect(document.nodes).toHaveLength(1); 1150 | expect(document.first.nodes).toHaveLength(2); 1151 | expect(document.first.raws).toEqual({ 1152 | isRuleLike: true, 1153 | styledSyntaxIsComponent: true, 1154 | styledSyntaxRangeStart: 27, 1155 | styledSyntaxRangeEnd: 90, 1156 | codeBefore: 'let Component = styled.div`', 1157 | codeAfter: '`;', 1158 | after: ' ${anotherThing}', 1159 | semicolon: true, 1160 | }); 1161 | 1162 | let firstNode = document.first.first; 1163 | let secondNode = firstNode.next(); 1164 | 1165 | expect(firstNode.prop).toBe('color'); 1166 | expect(firstNode.value).toBe('red'); 1167 | 1168 | expect(secondNode.prop).toBe('border-color'); 1169 | expect(secondNode.value).toBe('blue'); 1170 | 1171 | expect(secondNode.raws.before).toBe(' ${ borderWidth} '); 1172 | }); 1173 | 1174 | test('after comment (no semicolon)', () => { 1175 | let document = parse('let Component = styled.div`/* hello */ ${borderWidth}`;'); 1176 | 1177 | expect(document.nodes).toHaveLength(1); 1178 | expect(document.first.nodes).toHaveLength(1); 1179 | expect(document.first.raws).toEqual({ 1180 | isRuleLike: true, 1181 | styledSyntaxIsComponent: true, 1182 | styledSyntaxRangeStart: 27, 1183 | styledSyntaxRangeEnd: 53, 1184 | codeBefore: 'let Component = styled.div`', 1185 | codeAfter: '`;', 1186 | after: ' ${borderWidth}', 1187 | semicolon: false, 1188 | }); 1189 | 1190 | let firstNode = document.first.first; 1191 | 1192 | expect(firstNode.type).toBe('comment'); 1193 | expect(firstNode.text).toBe('hello'); 1194 | }); 1195 | 1196 | test('after comment (with semicolon)', () => { 1197 | let document = parse('let Component = styled.div`/* hello */ ${borderWidth};`;'); 1198 | 1199 | expect(document.nodes).toHaveLength(1); 1200 | expect(document.first.nodes).toHaveLength(1); 1201 | expect(document.first.raws).toEqual({ 1202 | isRuleLike: true, 1203 | styledSyntaxIsComponent: true, 1204 | styledSyntaxRangeStart: 27, 1205 | styledSyntaxRangeEnd: 54, 1206 | codeBefore: 'let Component = styled.div`', 1207 | codeAfter: '`;', 1208 | after: ' ${borderWidth};', 1209 | semicolon: false, 1210 | }); 1211 | 1212 | let firstNode = document.first.first; 1213 | 1214 | expect(firstNode.type).toBe('comment'); 1215 | expect(firstNode.text).toBe('hello'); 1216 | }); 1217 | 1218 | test('before comment (no semicolon, no space)', () => { 1219 | let document = parse('let Component = styled.div`${borderWidth}/* hello */`;'); 1220 | 1221 | expect(document.nodes).toHaveLength(1); 1222 | expect(document.first.nodes).toHaveLength(1); 1223 | expect(document.first.raws).toEqual({ 1224 | isRuleLike: true, 1225 | styledSyntaxIsComponent: true, 1226 | styledSyntaxRangeStart: 27, 1227 | styledSyntaxRangeEnd: 52, 1228 | codeBefore: 'let Component = styled.div`', 1229 | codeAfter: '`;', 1230 | after: '', 1231 | semicolon: false, 1232 | }); 1233 | 1234 | let firstNode = document.first.first; 1235 | 1236 | expect(firstNode.type).toBe('comment'); 1237 | expect(firstNode.text).toBe('hello'); 1238 | expect(firstNode.raws.before).toBe('${borderWidth}'); 1239 | }); 1240 | 1241 | test('before comment (no semicolon, with space)', () => { 1242 | let document = parse('let Component = styled.div`${borderWidth} /* hello */`;'); 1243 | 1244 | expect(document.nodes).toHaveLength(1); 1245 | expect(document.first.nodes).toHaveLength(1); 1246 | expect(document.first.raws).toEqual({ 1247 | isRuleLike: true, 1248 | styledSyntaxIsComponent: true, 1249 | styledSyntaxRangeStart: 27, 1250 | styledSyntaxRangeEnd: 53, 1251 | codeBefore: 'let Component = styled.div`', 1252 | codeAfter: '`;', 1253 | after: '', 1254 | semicolon: false, 1255 | }); 1256 | 1257 | let firstNode = document.first.first; 1258 | 1259 | expect(firstNode.type).toBe('comment'); 1260 | expect(firstNode.text).toBe('hello'); 1261 | expect(firstNode.raws.before).toBe('${borderWidth} '); 1262 | }); 1263 | 1264 | test('before comment (with semicolon, no space)', () => { 1265 | let document = parse('let Component = styled.div`${borderWidth};/* hello */`;'); 1266 | 1267 | expect(document.nodes).toHaveLength(1); 1268 | expect(document.first.nodes).toHaveLength(1); 1269 | expect(document.first.raws).toEqual({ 1270 | isRuleLike: true, 1271 | styledSyntaxIsComponent: true, 1272 | styledSyntaxRangeStart: 27, 1273 | styledSyntaxRangeEnd: 53, 1274 | codeBefore: 'let Component = styled.div`', 1275 | codeAfter: '`;', 1276 | after: '', 1277 | semicolon: false, 1278 | }); 1279 | 1280 | let firstNode = document.first.first; 1281 | 1282 | expect(firstNode.type).toBe('comment'); 1283 | expect(firstNode.text).toBe('hello'); 1284 | expect(firstNode.raws.before).toBe('${borderWidth};'); 1285 | }); 1286 | 1287 | test('before comment (with semicolon and space)', () => { 1288 | let document = parse('let Component = styled.div`${borderWidth}; /* hello */`;'); 1289 | 1290 | expect(document.nodes).toHaveLength(1); 1291 | expect(document.first.nodes).toHaveLength(1); 1292 | expect(document.first.raws).toEqual({ 1293 | isRuleLike: true, 1294 | styledSyntaxIsComponent: true, 1295 | styledSyntaxRangeStart: 27, 1296 | styledSyntaxRangeEnd: 54, 1297 | codeBefore: 'let Component = styled.div`', 1298 | codeAfter: '`;', 1299 | after: '', 1300 | semicolon: false, 1301 | }); 1302 | 1303 | let firstNode = document.first.first; 1304 | 1305 | expect(firstNode.type).toBe('comment'); 1306 | expect(firstNode.text).toBe('hello'); 1307 | expect(firstNode.raws.before).toBe('${borderWidth}; '); 1308 | }); 1309 | 1310 | test('before at-rule (with semicolon)', () => { 1311 | let document = parse('let Component = styled.div`${borderWidth};@media screen {}`;'); 1312 | 1313 | expect(document.nodes).toHaveLength(1); 1314 | expect(document.first.nodes).toHaveLength(1); 1315 | expect(document.first.raws).toEqual({ 1316 | isRuleLike: true, 1317 | styledSyntaxIsComponent: true, 1318 | styledSyntaxRangeStart: 27, 1319 | styledSyntaxRangeEnd: 58, 1320 | codeBefore: 'let Component = styled.div`', 1321 | codeAfter: '`;', 1322 | after: '', 1323 | semicolon: false, 1324 | }); 1325 | 1326 | let firstNode = document.first.first; 1327 | 1328 | expect(firstNode.type).toBe('atrule'); 1329 | expect(firstNode.name).toBe('media'); 1330 | expect(firstNode.params).toBe('screen'); 1331 | expect(firstNode.raws.before).toBe('${borderWidth};'); 1332 | expect(firstNode.nodes).toHaveLength(0); 1333 | }); 1334 | 1335 | test('before at-rule (without semicolon)', () => { 1336 | let document = parse('let Component = styled.div`${borderWidth} @media screen {}`;'); 1337 | 1338 | expect(document.nodes).toHaveLength(1); 1339 | expect(document.first.nodes).toHaveLength(1); 1340 | expect(document.first.raws).toEqual({ 1341 | isRuleLike: true, 1342 | styledSyntaxIsComponent: true, 1343 | styledSyntaxRangeStart: 27, 1344 | styledSyntaxRangeEnd: 58, 1345 | codeBefore: 'let Component = styled.div`', 1346 | codeAfter: '`;', 1347 | after: '', 1348 | semicolon: false, 1349 | }); 1350 | 1351 | let firstNode = document.first.first; 1352 | 1353 | expect(firstNode.type).toBe('atrule'); 1354 | expect(firstNode.name).toBe('media'); 1355 | expect(firstNode.params).toBe('screen'); 1356 | expect(firstNode.raws.before).toBe('${borderWidth} '); 1357 | expect(firstNode.nodes).toHaveLength(0); 1358 | }); 1359 | }); 1360 | 1361 | describe('multiple standalone interpolations', () => { 1362 | test('after declaration (no semicolon, no space after)', () => { 1363 | let document = parse( 1364 | 'let Component = styled.div`color: red; ${borderWidth}${borderWidth}`;', 1365 | ); 1366 | 1367 | expect(document.nodes).toHaveLength(1); 1368 | expect(document.first.nodes).toHaveLength(1); 1369 | expect(document.first.raws).toEqual({ 1370 | isRuleLike: true, 1371 | styledSyntaxIsComponent: true, 1372 | styledSyntaxRangeStart: 27, 1373 | styledSyntaxRangeEnd: 67, 1374 | codeBefore: 'let Component = styled.div`', 1375 | codeAfter: '`;', 1376 | after: ' ${borderWidth}${borderWidth}', 1377 | semicolon: true, 1378 | }); 1379 | 1380 | let firstNode = document.first.first; 1381 | 1382 | expect(firstNode.prop).toBe('color'); 1383 | expect(firstNode.value).toBe('red'); 1384 | }); 1385 | 1386 | test('after declaration (no semicolon, space after)', () => { 1387 | let document = parse( 1388 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth} `;', 1389 | ); 1390 | 1391 | expect(document.nodes).toHaveLength(1); 1392 | expect(document.first.nodes).toHaveLength(1); 1393 | expect(document.first.raws).toEqual({ 1394 | isRuleLike: true, 1395 | styledSyntaxIsComponent: true, 1396 | styledSyntaxRangeStart: 27, 1397 | styledSyntaxRangeEnd: 69, 1398 | codeBefore: 'let Component = styled.div`', 1399 | codeAfter: '`;', 1400 | after: ' ${borderWidth} ${borderWidth} ', 1401 | semicolon: true, 1402 | }); 1403 | 1404 | let firstNode = document.first.first; 1405 | 1406 | expect(firstNode.prop).toBe('color'); 1407 | expect(firstNode.value).toBe('red'); 1408 | }); 1409 | 1410 | test('after declaration (with semicolon)', () => { 1411 | let document = parse( 1412 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth};`;', 1413 | ); 1414 | 1415 | expect(document.nodes).toHaveLength(1); 1416 | expect(document.first.nodes).toHaveLength(1); 1417 | expect(document.first.raws).toEqual({ 1418 | isRuleLike: true, 1419 | styledSyntaxIsComponent: true, 1420 | styledSyntaxRangeStart: 27, 1421 | styledSyntaxRangeEnd: 69, 1422 | codeBefore: 'let Component = styled.div`', 1423 | codeAfter: '`;', 1424 | after: ' ${borderWidth} ${borderWidth};', 1425 | semicolon: true, 1426 | }); 1427 | 1428 | let firstNode = document.first.first; 1429 | 1430 | expect(firstNode.prop).toBe('color'); 1431 | expect(firstNode.value).toBe('red'); 1432 | }); 1433 | 1434 | test('before declaration (no semicolon, no space)', () => { 1435 | let document = parse( 1436 | 'let Component = styled.div`${borderWidth} ${background}color: red`;', 1437 | ); 1438 | 1439 | expect(document.nodes).toHaveLength(1); 1440 | expect(document.first.nodes).toHaveLength(1); 1441 | expect(document.first.raws).toEqual({ 1442 | isRuleLike: true, 1443 | styledSyntaxIsComponent: true, 1444 | styledSyntaxRangeStart: 27, 1445 | styledSyntaxRangeEnd: 65, 1446 | codeBefore: 'let Component = styled.div`', 1447 | codeAfter: '`;', 1448 | after: '', 1449 | semicolon: false, 1450 | }); 1451 | 1452 | let firstNode = document.first.first; 1453 | 1454 | expect(firstNode.prop).toBe('${background}color'); 1455 | expect(firstNode.value).toBe('red'); 1456 | expect(firstNode.raws.before).toBe('${borderWidth} '); 1457 | }); 1458 | 1459 | test('before declaration (no semicolon, with space)', () => { 1460 | let document = parse( 1461 | 'let Component = styled.div`${borderWidth}${background} color: red`;', 1462 | ); 1463 | 1464 | expect(document.nodes).toHaveLength(1); 1465 | expect(document.first.nodes).toHaveLength(1); 1466 | expect(document.first.raws).toEqual({ 1467 | isRuleLike: true, 1468 | styledSyntaxIsComponent: true, 1469 | styledSyntaxRangeStart: 27, 1470 | styledSyntaxRangeEnd: 65, 1471 | codeBefore: 'let Component = styled.div`', 1472 | codeAfter: '`;', 1473 | after: '', 1474 | semicolon: false, 1475 | }); 1476 | 1477 | let firstNode = document.first.first; 1478 | 1479 | expect(firstNode.prop).toBe('color'); 1480 | expect(firstNode.value).toBe('red'); 1481 | expect(firstNode.raws.before).toBe('${borderWidth}${background} '); 1482 | }); 1483 | 1484 | test('before declaration (no semicolon, with space, three interpolations)', () => { 1485 | let document = parse( 1486 | 'let Component = styled.div`${borderWidth}${background}${display} color: red`;', 1487 | ); 1488 | 1489 | expect(document.nodes).toHaveLength(1); 1490 | expect(document.first.nodes).toHaveLength(1); 1491 | expect(document.first.raws).toEqual({ 1492 | isRuleLike: true, 1493 | styledSyntaxIsComponent: true, 1494 | styledSyntaxRangeStart: 27, 1495 | styledSyntaxRangeEnd: 75, 1496 | codeBefore: 'let Component = styled.div`', 1497 | codeAfter: '`;', 1498 | after: '', 1499 | semicolon: false, 1500 | }); 1501 | 1502 | let firstNode = document.first.first; 1503 | 1504 | expect(firstNode.prop).toBe('color'); 1505 | expect(firstNode.value).toBe('red'); 1506 | expect(firstNode.raws.before).toBe('${borderWidth}${background}${display} '); 1507 | }); 1508 | 1509 | test('before declaration (with space)', () => { 1510 | let document = parse( 1511 | 'let Component = styled.div`${borderWidth} ${borderWidth} color: red`;', 1512 | ); 1513 | 1514 | expect(document.nodes).toHaveLength(1); 1515 | expect(document.first.nodes).toHaveLength(1); 1516 | expect(document.first.raws).toEqual({ 1517 | isRuleLike: true, 1518 | styledSyntaxIsComponent: true, 1519 | styledSyntaxRangeStart: 27, 1520 | styledSyntaxRangeEnd: 67, 1521 | codeBefore: 'let Component = styled.div`', 1522 | codeAfter: '`;', 1523 | after: '', 1524 | semicolon: false, 1525 | }); 1526 | 1527 | let firstNode = document.first.first; 1528 | 1529 | expect(firstNode.prop).toBe('color'); 1530 | expect(firstNode.value).toBe('red'); 1531 | expect(firstNode.raws.before).toBe('${borderWidth} ${borderWidth} '); 1532 | }); 1533 | 1534 | test('before declaration (with semicolon)', () => { 1535 | let document = parse( 1536 | 'let Component = styled.div`${borderWidth}${borderWidth};color: red`;', 1537 | ); 1538 | 1539 | expect(document.nodes).toHaveLength(1); 1540 | expect(document.first.nodes).toHaveLength(1); 1541 | expect(document.first.raws).toEqual({ 1542 | isRuleLike: true, 1543 | styledSyntaxIsComponent: true, 1544 | styledSyntaxRangeStart: 27, 1545 | styledSyntaxRangeEnd: 66, 1546 | codeBefore: 'let Component = styled.div`', 1547 | codeAfter: '`;', 1548 | after: '', 1549 | semicolon: false, 1550 | }); 1551 | 1552 | let firstNode = document.first.first; 1553 | 1554 | expect(firstNode.prop).toBe('color'); 1555 | expect(firstNode.value).toBe('red'); 1556 | expect(firstNode.raws.before).toBe('${borderWidth}${borderWidth};'); 1557 | }); 1558 | 1559 | test('before declaration (with semicolon and space)', () => { 1560 | let document = parse( 1561 | 'let Component = styled.div`${borderWidth}${borderWidth}; color: red`;', 1562 | ); 1563 | 1564 | expect(document.nodes).toHaveLength(1); 1565 | expect(document.first.nodes).toHaveLength(1); 1566 | expect(document.first.raws).toEqual({ 1567 | isRuleLike: true, 1568 | styledSyntaxIsComponent: true, 1569 | styledSyntaxRangeStart: 27, 1570 | styledSyntaxRangeEnd: 67, 1571 | codeBefore: 'let Component = styled.div`', 1572 | codeAfter: '`;', 1573 | after: '', 1574 | semicolon: false, 1575 | }); 1576 | 1577 | let firstNode = document.first.first; 1578 | 1579 | expect(firstNode.prop).toBe('color'); 1580 | expect(firstNode.value).toBe('red'); 1581 | expect(firstNode.raws.before).toBe('${borderWidth}${borderWidth}; '); 1582 | }); 1583 | 1584 | test('between two declarations (no semicolon)', () => { 1585 | let document = parse( 1586 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth} border-color: blue;`;', 1587 | ); 1588 | 1589 | expect(document.nodes).toHaveLength(1); 1590 | expect(document.first.nodes).toHaveLength(2); 1591 | expect(document.first.raws).toEqual({ 1592 | isRuleLike: true, 1593 | styledSyntaxIsComponent: true, 1594 | styledSyntaxRangeStart: 27, 1595 | styledSyntaxRangeEnd: 88, 1596 | codeBefore: 'let Component = styled.div`', 1597 | codeAfter: '`;', 1598 | after: '', 1599 | semicolon: true, 1600 | }); 1601 | 1602 | let firstNode = document.first.first; 1603 | let secondNode = firstNode.next(); 1604 | 1605 | expect(firstNode.prop).toBe('color'); 1606 | expect(firstNode.value).toBe('red'); 1607 | 1608 | expect(secondNode.prop).toBe('border-color'); 1609 | expect(secondNode.value).toBe('blue'); 1610 | 1611 | expect(secondNode.raws.before).toBe(' ${borderWidth} ${borderWidth} '); 1612 | }); 1613 | 1614 | test('between two declarations (with semicolon)', () => { 1615 | let document = parse( 1616 | 'let Component = styled.div`color: red; ${borderWidth} ${borderWidth}; border-color: blue;`;', 1617 | ); 1618 | 1619 | expect(document.nodes).toHaveLength(1); 1620 | expect(document.first.nodes).toHaveLength(2); 1621 | expect(document.first.raws).toEqual({ 1622 | isRuleLike: true, 1623 | styledSyntaxIsComponent: true, 1624 | styledSyntaxRangeStart: 27, 1625 | styledSyntaxRangeEnd: 89, 1626 | codeBefore: 'let Component = styled.div`', 1627 | codeAfter: '`;', 1628 | after: '', 1629 | semicolon: true, 1630 | }); 1631 | 1632 | let firstNode = document.first.first; 1633 | let secondNode = firstNode.next(); 1634 | 1635 | expect(firstNode.prop).toBe('color'); 1636 | expect(firstNode.value).toBe('red'); 1637 | 1638 | expect(secondNode.prop).toBe('border-color'); 1639 | expect(secondNode.value).toBe('blue'); 1640 | 1641 | expect(secondNode.raws.before).toBe(' ${borderWidth} ${borderWidth}; '); 1642 | }); 1643 | 1644 | test('before comment (no semicolon, with space)', () => { 1645 | let document = parse( 1646 | 'let Component = styled.div`${borderWidth}${background} /* hello */`;', 1647 | ); 1648 | 1649 | expect(document.nodes).toHaveLength(1); 1650 | expect(document.first.nodes).toHaveLength(1); 1651 | expect(document.first.raws).toEqual({ 1652 | isRuleLike: true, 1653 | styledSyntaxIsComponent: true, 1654 | styledSyntaxRangeStart: 27, 1655 | styledSyntaxRangeEnd: 66, 1656 | codeBefore: 'let Component = styled.div`', 1657 | codeAfter: '`;', 1658 | after: '', 1659 | semicolon: false, 1660 | }); 1661 | 1662 | let firstNode = document.first.first; 1663 | 1664 | expect(firstNode.type).toBe('comment'); 1665 | expect(firstNode.text).toBe('hello'); 1666 | expect(firstNode.raws.before).toBe('${borderWidth}${background} '); 1667 | }); 1668 | 1669 | test('as the only content (no semicolon)', () => { 1670 | let document = parse('let Component = styled.div`${color}${color}`;'); 1671 | 1672 | expect(document.nodes).toHaveLength(1); 1673 | expect(document.first.nodes).toHaveLength(0); 1674 | expect(document.first.raws).toEqual({ 1675 | isRuleLike: true, 1676 | styledSyntaxIsComponent: true, 1677 | styledSyntaxRangeStart: 27, 1678 | styledSyntaxRangeEnd: 43, 1679 | codeBefore: 'let Component = styled.div`', 1680 | codeAfter: '`;', 1681 | after: '${color}${color}', 1682 | }); 1683 | }); 1684 | 1685 | test('as the only content (with semicolon)', () => { 1686 | let document = parse('let Component = styled.div`${color}${color};`;'); 1687 | 1688 | expect(document.nodes).toHaveLength(1); 1689 | expect(document.first.nodes).toHaveLength(0); 1690 | expect(document.first.raws).toEqual({ 1691 | isRuleLike: true, 1692 | styledSyntaxIsComponent: true, 1693 | styledSyntaxRangeStart: 27, 1694 | styledSyntaxRangeEnd: 44, 1695 | codeBefore: 'let Component = styled.div`', 1696 | codeAfter: '`;', 1697 | after: '${color}${color};', 1698 | }); 1699 | }); 1700 | }); 1701 | 1702 | describe('inside a rule', () => { 1703 | describe('selectors', () => { 1704 | test('in selector (whole selector)', () => { 1705 | let document = parse( 1706 | 'let Component = styled.div`div{${Component} { color: red; }}`;', 1707 | ); 1708 | 1709 | expect(document.nodes).toHaveLength(1); 1710 | expect(document.first.nodes).toHaveLength(1); 1711 | expect(document.first.raws).toEqual({ 1712 | isRuleLike: true, 1713 | styledSyntaxIsComponent: true, 1714 | styledSyntaxRangeStart: 27, 1715 | styledSyntaxRangeEnd: 60, 1716 | codeBefore: 'let Component = styled.div`', 1717 | codeAfter: '`;', 1718 | after: '', 1719 | semicolon: false, 1720 | }); 1721 | 1722 | let firstNode = document.first.first; 1723 | 1724 | expect(firstNode.type).toBe('rule'); 1725 | expect(firstNode.selector).toBe('div'); 1726 | expect(firstNode.nodes).toHaveLength(1); 1727 | 1728 | let firstInsideNode = firstNode.first; 1729 | 1730 | expect(firstInsideNode.type).toBe('rule'); 1731 | expect(firstInsideNode.selector).toBe('${Component}'); 1732 | expect(firstInsideNode.nodes).toHaveLength(1); 1733 | }); 1734 | 1735 | test('in selector (first part is interpolation, one selector)', () => { 1736 | let document = parse( 1737 | 'let Component = styled.div`div{ ${Component} a { color: red; } }`;', 1738 | ); 1739 | 1740 | expect(document.nodes).toHaveLength(1); 1741 | expect(document.first.nodes).toHaveLength(1); 1742 | expect(document.first.raws).toEqual({ 1743 | isRuleLike: true, 1744 | styledSyntaxIsComponent: true, 1745 | styledSyntaxRangeStart: 27, 1746 | styledSyntaxRangeEnd: 64, 1747 | codeBefore: 'let Component = styled.div`', 1748 | codeAfter: '`;', 1749 | after: '', 1750 | semicolon: false, 1751 | }); 1752 | 1753 | let firstNode = document.first.first; 1754 | 1755 | expect(firstNode.type).toBe('rule'); 1756 | expect(firstNode.selector).toBe('div'); 1757 | expect(firstNode.nodes).toHaveLength(1); 1758 | 1759 | let firstInsideNode = firstNode.first; 1760 | 1761 | expect(firstInsideNode.type).toBe('rule'); 1762 | expect(firstInsideNode.selector).toBe('${Component} a'); 1763 | expect(firstInsideNode.nodes).toHaveLength(1); 1764 | }); 1765 | 1766 | test('interpolation with semicolon before selector', () => { 1767 | let document = parse( 1768 | 'let Component = styled.div`div {${Component}; a { color: red; }}`;', 1769 | ); 1770 | 1771 | expect(document.nodes).toHaveLength(1); 1772 | expect(document.first.nodes).toHaveLength(1); 1773 | expect(document.first.raws).toEqual({ 1774 | isRuleLike: true, 1775 | styledSyntaxIsComponent: true, 1776 | styledSyntaxRangeStart: 27, 1777 | styledSyntaxRangeEnd: 64, 1778 | codeBefore: 'let Component = styled.div`', 1779 | codeAfter: '`;', 1780 | after: '', 1781 | semicolon: false, 1782 | }); 1783 | 1784 | let firstNode = document.first.first; 1785 | 1786 | expect(firstNode.type).toBe('rule'); 1787 | expect(firstNode.selector).toBe('div'); 1788 | expect(firstNode.nodes).toHaveLength(1); 1789 | 1790 | let firstInsideNode = firstNode.first; 1791 | 1792 | expect(firstInsideNode.type).toBe('rule'); 1793 | expect(firstInsideNode.selector).toBe('a'); 1794 | expect(firstInsideNode.nodes).toHaveLength(1); 1795 | expect(firstInsideNode.raws.before).toBe('${Component}; '); 1796 | }); 1797 | }); 1798 | 1799 | describe('standalone interpolation', () => { 1800 | test('after declaration (no semicolon, space after)', () => { 1801 | let document = parse( 1802 | 'let Component = styled.div`div{color: red; ${borderWidth} }`;', 1803 | ); 1804 | 1805 | expect(document.nodes).toHaveLength(1); 1806 | expect(document.first.nodes).toHaveLength(1); 1807 | expect(document.first.raws).toEqual({ 1808 | isRuleLike: true, 1809 | styledSyntaxIsComponent: true, 1810 | styledSyntaxRangeStart: 27, 1811 | styledSyntaxRangeEnd: 59, 1812 | codeBefore: 'let Component = styled.div`', 1813 | codeAfter: '`;', 1814 | after: '', 1815 | semicolon: false, 1816 | }); 1817 | 1818 | let firstNode = document.first.first; 1819 | 1820 | expect(firstNode.type).toBe('rule'); 1821 | expect(firstNode.selector).toBe('div'); 1822 | expect(firstNode.nodes).toHaveLength(1); 1823 | expect(firstNode.raws).toEqual({ 1824 | before: '', 1825 | between: '', 1826 | after: ' ${borderWidth} ', 1827 | semicolon: true, 1828 | }); 1829 | 1830 | let firstInsideNode = firstNode.first; 1831 | 1832 | expect(firstInsideNode.prop).toBe('color'); 1833 | expect(firstInsideNode.value).toBe('red'); 1834 | }); 1835 | 1836 | test('after declaration (with semicolon)', () => { 1837 | let document = parse( 1838 | 'let Component = styled.div`div{color: red; ${borderWidth};}`;', 1839 | ); 1840 | 1841 | expect(document.nodes).toHaveLength(1); 1842 | expect(document.first.nodes).toHaveLength(1); 1843 | expect(document.first.raws).toEqual({ 1844 | isRuleLike: true, 1845 | styledSyntaxIsComponent: true, 1846 | styledSyntaxRangeStart: 27, 1847 | styledSyntaxRangeEnd: 59, 1848 | codeBefore: 'let Component = styled.div`', 1849 | codeAfter: '`;', 1850 | after: '', 1851 | semicolon: false, 1852 | }); 1853 | 1854 | let firstNode = document.first.first; 1855 | 1856 | expect(firstNode.type).toBe('rule'); 1857 | expect(firstNode.selector).toBe('div'); 1858 | expect(firstNode.nodes).toHaveLength(1); 1859 | expect(firstNode.raws).toEqual({ 1860 | before: '', 1861 | between: '', 1862 | after: ' ${borderWidth};', 1863 | semicolon: true, 1864 | }); 1865 | 1866 | let firstInsideNode = firstNode.first; 1867 | 1868 | expect(firstInsideNode.prop).toBe('color'); 1869 | expect(firstInsideNode.value).toBe('red'); 1870 | }); 1871 | 1872 | test('throws for non-interpolation word after declaration', () => { 1873 | expect(() => { 1874 | parse( 1875 | 'let Component = styled.div`div{color: red; ${borderWidth} notInterpolation}`;', 1876 | ); 1877 | }).toThrow(/:1:59: Unknown word/); 1878 | }); 1879 | 1880 | test('as the only content (no semicolon)', () => { 1881 | let document = parse('let Component = styled.div`div{${color}}`;'); 1882 | 1883 | expect(document.nodes).toHaveLength(1); 1884 | expect(document.first.nodes).toHaveLength(1); 1885 | expect(document.first.raws).toEqual({ 1886 | isRuleLike: true, 1887 | styledSyntaxIsComponent: true, 1888 | styledSyntaxRangeStart: 27, 1889 | styledSyntaxRangeEnd: 40, 1890 | codeBefore: 'let Component = styled.div`', 1891 | codeAfter: '`;', 1892 | after: '', 1893 | semicolon: false, 1894 | }); 1895 | 1896 | let firstNode = document.first.first; 1897 | 1898 | expect(firstNode.type).toBe('rule'); 1899 | expect(firstNode.selector).toBe('div'); 1900 | expect(firstNode.nodes).toHaveLength(0); 1901 | expect(firstNode.raws).toEqual({ 1902 | before: '', 1903 | between: '', 1904 | after: '${color}', 1905 | }); 1906 | }); 1907 | 1908 | test('as the only content (with semicolon)', () => { 1909 | let document = parse('let Component = styled.div`div{${color};}`;'); 1910 | 1911 | expect(document.nodes).toHaveLength(1); 1912 | expect(document.first.nodes).toHaveLength(1); 1913 | expect(document.first.raws).toEqual({ 1914 | isRuleLike: true, 1915 | styledSyntaxIsComponent: true, 1916 | styledSyntaxRangeStart: 27, 1917 | styledSyntaxRangeEnd: 41, 1918 | codeBefore: 'let Component = styled.div`', 1919 | codeAfter: '`;', 1920 | after: '', 1921 | semicolon: false, 1922 | }); 1923 | 1924 | let firstNode = document.first.first; 1925 | 1926 | expect(firstNode.type).toBe('rule'); 1927 | expect(firstNode.selector).toBe('div'); 1928 | expect(firstNode.nodes).toHaveLength(0); 1929 | expect(firstNode.raws).toEqual({ 1930 | before: '', 1931 | between: '', 1932 | after: '${color};', 1933 | }); 1934 | }); 1935 | }); 1936 | }); 1937 | }); 1938 | 1939 | describe('interpolations with css helper (one level deep)', () => { 1940 | test('single at the end', () => { 1941 | let document = parse('let Component = styled.div`color: green;${css`color: red`}`;'); 1942 | 1943 | expect(document.nodes).toHaveLength(2); 1944 | 1945 | let firstComponent = document.first; 1946 | 1947 | expect(firstComponent.nodes).toHaveLength(1); 1948 | expect(firstComponent.raws).toEqual({ 1949 | isRuleLike: true, 1950 | styledSyntaxIsComponent: true, 1951 | styledSyntaxRangeStart: 27, 1952 | styledSyntaxRangeEnd: 58, 1953 | codeBefore: 'let Component = styled.div`', 1954 | codeAfter: '`;', 1955 | after: '${css`color: red`}', 1956 | semicolon: true, 1957 | }); 1958 | expect(firstComponent.source.start).toEqual({ 1959 | offset: 27, 1960 | line: 1, 1961 | column: 28, 1962 | }); 1963 | 1964 | expect(firstComponent.first.prop).toBe('color'); 1965 | expect(firstComponent.first.value).toBe('green'); 1966 | expect(firstComponent.first.source.start).toEqual({ 1967 | offset: 27, 1968 | line: 1, 1969 | column: 28, 1970 | }); 1971 | expect(firstComponent.first.source.end).toEqual({ 1972 | offset: 39, 1973 | line: 1, 1974 | column: 40, 1975 | }); 1976 | 1977 | let cssInterpolation = document.first.next(); 1978 | 1979 | expect(cssInterpolation.nodes).toHaveLength(1); 1980 | expect(cssInterpolation.raws).toEqual({ 1981 | after: '', 1982 | semicolon: false, 1983 | isRuleLike: true, 1984 | styledOriginalContent: 'color: red', 1985 | styledSyntaxRangeEnd: 56, 1986 | styledSyntaxRangeStart: 46, 1987 | }); 1988 | expect(cssInterpolation.source.start).toEqual({ 1989 | offset: 46, 1990 | line: 1, 1991 | column: 47, 1992 | }); 1993 | 1994 | expect(cssInterpolation.first.prop).toBe('color'); 1995 | expect(cssInterpolation.first.value).toBe('red'); 1996 | expect(cssInterpolation.first.source.start).toEqual({ 1997 | offset: 46, 1998 | line: 1, 1999 | column: 47, 2000 | }); 2001 | expect(cssInterpolation.first.source.end).toEqual({ 2002 | offset: 55, 2003 | line: 1, 2004 | column: 56, 2005 | }); 2006 | }); 2007 | 2008 | test('single at the start', () => { 2009 | let document = parse('let Component = styled.div`${css`color: red`} color: green;`;'); 2010 | 2011 | expect(document.nodes).toHaveLength(2); 2012 | 2013 | let firstComponent = document.first; 2014 | 2015 | expect(firstComponent.nodes).toHaveLength(1); 2016 | expect(firstComponent.raws).toEqual({ 2017 | isRuleLike: true, 2018 | styledSyntaxIsComponent: true, 2019 | styledSyntaxRangeStart: 27, 2020 | styledSyntaxRangeEnd: 59, 2021 | codeBefore: 'let Component = styled.div`', 2022 | codeAfter: '`;', 2023 | after: '', 2024 | semicolon: true, 2025 | }); 2026 | 2027 | let firstNode = firstComponent.first; 2028 | 2029 | expect(firstNode.prop).toBe('color'); 2030 | expect(firstNode.value).toBe('green'); 2031 | expect(firstNode.raws.before).toBe('${css`color: red`} '); 2032 | 2033 | let cssInterpolation = document.first.next(); 2034 | 2035 | expect(cssInterpolation.nodes).toHaveLength(1); 2036 | expect(cssInterpolation.first.prop).toBe('color'); 2037 | expect(cssInterpolation.first.value).toBe('red'); 2038 | expect(cssInterpolation.raws).toEqual({ 2039 | after: '', 2040 | semicolon: false, 2041 | isRuleLike: true, 2042 | styledOriginalContent: 'color: red', 2043 | styledSyntaxRangeEnd: 43, 2044 | styledSyntaxRangeStart: 33, 2045 | }); 2046 | }); 2047 | 2048 | test('single at the start and at the end', () => { 2049 | let document = parse( 2050 | 'let Component = styled.div`${css`color: red`} color: green;${css`color: blue`}`;', 2051 | ); 2052 | 2053 | expect(document.nodes).toHaveLength(3); 2054 | 2055 | let firstComponent = document.first; 2056 | 2057 | expect(firstComponent.nodes).toHaveLength(1); 2058 | expect(firstComponent.raws).toEqual({ 2059 | isRuleLike: true, 2060 | styledSyntaxIsComponent: true, 2061 | styledSyntaxRangeStart: 27, 2062 | styledSyntaxRangeEnd: 78, 2063 | codeBefore: 'let Component = styled.div`', 2064 | codeAfter: '`;', 2065 | after: '${css`color: blue`}', 2066 | semicolon: true, 2067 | }); 2068 | 2069 | let firstNode = firstComponent.first; 2070 | 2071 | expect(firstNode.prop).toBe('color'); 2072 | expect(firstNode.value).toBe('green'); 2073 | expect(firstNode.raws.before).toBe('${css`color: red`} '); 2074 | 2075 | let cssInterpolationOne = document.first.next(); 2076 | 2077 | expect(cssInterpolationOne.nodes).toHaveLength(1); 2078 | expect(cssInterpolationOne.first.prop).toBe('color'); 2079 | expect(cssInterpolationOne.first.value).toBe('red'); 2080 | expect(cssInterpolationOne.raws).toEqual({ 2081 | after: '', 2082 | semicolon: false, 2083 | isRuleLike: true, 2084 | styledOriginalContent: 'color: red', 2085 | styledSyntaxRangeEnd: 43, 2086 | styledSyntaxRangeStart: 33, 2087 | }); 2088 | 2089 | let cssInterpolationTwo = document.last; 2090 | 2091 | expect(cssInterpolationTwo.nodes).toHaveLength(1); 2092 | expect(cssInterpolationTwo.raws).toEqual({ 2093 | after: '', 2094 | semicolon: false, 2095 | isRuleLike: true, 2096 | styledOriginalContent: 'color: blue', 2097 | styledSyntaxRangeEnd: 76, 2098 | styledSyntaxRangeStart: 65, 2099 | }); 2100 | 2101 | expect(cssInterpolationTwo.first.prop).toBe('color'); 2102 | expect(cssInterpolationTwo.first.value).toBe('blue'); 2103 | }); 2104 | 2105 | test('only two interpolations', () => { 2106 | let document = parse('let Component = styled.div`${css`color: red`}${css`color: blue`}`;'); 2107 | 2108 | expect(document.nodes).toHaveLength(3); 2109 | 2110 | let firstComponent = document.first; 2111 | 2112 | expect(firstComponent.nodes).toHaveLength(0); 2113 | expect(firstComponent.raws).toEqual({ 2114 | isRuleLike: true, 2115 | styledSyntaxIsComponent: true, 2116 | styledSyntaxRangeStart: 27, 2117 | styledSyntaxRangeEnd: 64, 2118 | codeBefore: 'let Component = styled.div`', 2119 | codeAfter: '`;', 2120 | after: '${css`color: red`}${css`color: blue`}', 2121 | }); 2122 | 2123 | let cssInterpolationOne = document.first.next(); 2124 | 2125 | expect(cssInterpolationOne.nodes).toHaveLength(1); 2126 | expect(cssInterpolationOne.first.prop).toBe('color'); 2127 | expect(cssInterpolationOne.first.value).toBe('red'); 2128 | expect(cssInterpolationOne.raws).toEqual({ 2129 | after: '', 2130 | semicolon: false, 2131 | isRuleLike: true, 2132 | styledOriginalContent: 'color: red', 2133 | styledSyntaxRangeEnd: 43, 2134 | styledSyntaxRangeStart: 33, 2135 | }); 2136 | 2137 | let cssInterpolationTwo = document.last; 2138 | 2139 | expect(cssInterpolationTwo.nodes).toHaveLength(1); 2140 | expect(cssInterpolationTwo.raws).toEqual({ 2141 | after: '', 2142 | semicolon: false, 2143 | isRuleLike: true, 2144 | styledOriginalContent: 'color: blue', 2145 | styledSyntaxRangeEnd: 62, 2146 | styledSyntaxRangeStart: 51, 2147 | }); 2148 | 2149 | expect(cssInterpolationTwo.first.prop).toBe('color'); 2150 | expect(cssInterpolationTwo.first.value).toBe('blue'); 2151 | }); 2152 | }); 2153 | 2154 | describe('interpolations with css helper (many levels deep)', () => { 2155 | test('two levels deep at the end', () => { 2156 | let document = parse('let Component = styled.div`${css`color: red;${css`color: blue`}`}`;'); 2157 | 2158 | expect(document.nodes).toHaveLength(3); 2159 | 2160 | let firstComponent = document.first; 2161 | 2162 | expect(firstComponent.raws).toEqual({ 2163 | isRuleLike: true, 2164 | styledSyntaxIsComponent: true, 2165 | styledSyntaxRangeStart: 27, 2166 | styledSyntaxRangeEnd: 65, 2167 | codeBefore: 'let Component = styled.div`', 2168 | codeAfter: '`;', 2169 | after: '${css`color: red;${css`color: blue`}`}', 2170 | }); 2171 | expect(firstComponent.nodes).toHaveLength(0); 2172 | 2173 | let cssInterpolationFirst = document.first.next(); 2174 | 2175 | expect(cssInterpolationFirst.nodes).toHaveLength(1); 2176 | expect(cssInterpolationFirst.raws).toEqual({ 2177 | after: '${css`color: blue`}', 2178 | semicolon: true, 2179 | isRuleLike: true, 2180 | styledOriginalContent: 'color: red;${css`color: blue`}', 2181 | styledSyntaxRangeEnd: 63, 2182 | styledSyntaxRangeStart: 33, 2183 | }); 2184 | 2185 | expect(cssInterpolationFirst.first.prop).toBe('color'); 2186 | expect(cssInterpolationFirst.first.value).toBe('red'); 2187 | 2188 | let cssInterpolationSecond = cssInterpolationFirst.next(); 2189 | 2190 | expect(cssInterpolationSecond.nodes).toHaveLength(1); 2191 | expect(cssInterpolationSecond.raws).toEqual({ 2192 | after: '', 2193 | semicolon: false, 2194 | isRuleLike: true, 2195 | styledOriginalContent: 'color: blue', 2196 | styledSyntaxRangeEnd: 61, 2197 | styledSyntaxRangeStart: 50, 2198 | }); 2199 | 2200 | expect(cssInterpolationSecond.first.prop).toBe('color'); 2201 | expect(cssInterpolationSecond.first.value).toBe('blue'); 2202 | }); 2203 | 2204 | test('three levels deep at the end', () => { 2205 | let document = parse( 2206 | 'let Component = styled.div`${css`color: red;${css`color: blue;${css`color: green;`}`}`}`;', 2207 | ); 2208 | 2209 | expect(document.nodes).toHaveLength(4); 2210 | 2211 | let firstComponent = document.first; 2212 | 2213 | expect(firstComponent.raws).toEqual({ 2214 | isRuleLike: true, 2215 | styledSyntaxIsComponent: true, 2216 | styledSyntaxRangeStart: 27, 2217 | styledSyntaxRangeEnd: 87, 2218 | codeBefore: 'let Component = styled.div`', 2219 | codeAfter: '`;', 2220 | after: '${css`color: red;${css`color: blue;${css`color: green;`}`}`}', 2221 | }); 2222 | expect(firstComponent.nodes).toHaveLength(0); 2223 | 2224 | let cssInterpolationFirst = document.first.next(); 2225 | 2226 | expect(cssInterpolationFirst.nodes).toHaveLength(1); 2227 | expect(cssInterpolationFirst.raws).toEqual({ 2228 | after: '${css`color: blue;${css`color: green;`}`}', 2229 | semicolon: true, 2230 | isRuleLike: true, 2231 | styledOriginalContent: 'color: red;${css`color: blue;${css`color: green;`}`}', 2232 | styledSyntaxRangeEnd: 85, 2233 | styledSyntaxRangeStart: 33, 2234 | }); 2235 | 2236 | expect(cssInterpolationFirst.first.prop).toBe('color'); 2237 | expect(cssInterpolationFirst.first.value).toBe('red'); 2238 | 2239 | let cssInterpolationSecond = cssInterpolationFirst.next(); 2240 | 2241 | expect(cssInterpolationSecond.nodes).toHaveLength(1); 2242 | expect(cssInterpolationSecond.raws).toEqual({ 2243 | after: '${css`color: green;`}', 2244 | semicolon: true, 2245 | isRuleLike: true, 2246 | styledOriginalContent: 'color: blue;${css`color: green;`}', 2247 | styledSyntaxRangeEnd: 83, 2248 | styledSyntaxRangeStart: 50, 2249 | }); 2250 | 2251 | expect(cssInterpolationSecond.first.prop).toBe('color'); 2252 | expect(cssInterpolationSecond.first.value).toBe('blue'); 2253 | 2254 | let cssInterpolationThird = cssInterpolationSecond.next(); 2255 | 2256 | expect(cssInterpolationThird.nodes).toHaveLength(1); 2257 | expect(cssInterpolationThird.raws).toEqual({ 2258 | after: '', 2259 | semicolon: true, 2260 | isRuleLike: true, 2261 | styledOriginalContent: 'color: green;', 2262 | styledSyntaxRangeEnd: 81, 2263 | styledSyntaxRangeStart: 68, 2264 | }); 2265 | 2266 | expect(cssInterpolationThird.first.prop).toBe('color'); 2267 | expect(cssInterpolationThird.first.value).toBe('green'); 2268 | }); 2269 | 2270 | test('two levels at the beginning', () => { 2271 | let document = parse( 2272 | 'let Component = styled.div`${css`${css`color: blue`} color: red;`}`;', 2273 | ); 2274 | 2275 | expect(document.nodes).toHaveLength(3); 2276 | 2277 | let firstComponent = document.first; 2278 | 2279 | expect(firstComponent.raws).toEqual({ 2280 | isRuleLike: true, 2281 | styledSyntaxIsComponent: true, 2282 | styledSyntaxRangeStart: 27, 2283 | styledSyntaxRangeEnd: 66, 2284 | codeBefore: 'let Component = styled.div`', 2285 | codeAfter: '`;', 2286 | after: '${css`${css`color: blue`} color: red;`}', 2287 | }); 2288 | expect(firstComponent.nodes).toHaveLength(0); 2289 | 2290 | let cssInterpolationFirst = document.first.next(); 2291 | 2292 | expect(cssInterpolationFirst.nodes).toHaveLength(1); 2293 | expect(cssInterpolationFirst.raws).toEqual({ 2294 | after: '', 2295 | semicolon: true, 2296 | isRuleLike: true, 2297 | styledOriginalContent: '${css`color: blue`} color: red;', 2298 | styledSyntaxRangeEnd: 64, 2299 | styledSyntaxRangeStart: 33, 2300 | }); 2301 | 2302 | expect(cssInterpolationFirst.first.prop).toBe('color'); 2303 | expect(cssInterpolationFirst.first.value).toBe('red'); 2304 | expect(cssInterpolationFirst.first.raws.before).toBe('${css`color: blue`} '); 2305 | 2306 | let cssInterpolationSecond = cssInterpolationFirst.next(); 2307 | 2308 | expect(cssInterpolationSecond.nodes).toHaveLength(1); 2309 | expect(cssInterpolationSecond.raws).toEqual({ 2310 | after: '', 2311 | semicolon: false, 2312 | isRuleLike: true, 2313 | styledOriginalContent: 'color: blue', 2314 | styledSyntaxRangeEnd: 50, 2315 | styledSyntaxRangeStart: 39, 2316 | }); 2317 | 2318 | expect(cssInterpolationSecond.first.prop).toBe('color'); 2319 | expect(cssInterpolationSecond.first.value).toBe('blue'); 2320 | }); 2321 | }); 2322 | 2323 | describe('interpolations with props', () => { 2324 | test('props with no styled helpers (at the end)', () => { 2325 | let document = parse('let Component = styled.div`color: green;${props => `color: red`}`;'); 2326 | 2327 | expect(document.nodes).toHaveLength(1); 2328 | 2329 | let firstComponent = document.first; 2330 | 2331 | expect(firstComponent.raws).toEqual({ 2332 | isRuleLike: true, 2333 | styledSyntaxIsComponent: true, 2334 | styledSyntaxRangeStart: 27, 2335 | styledSyntaxRangeEnd: 64, 2336 | codeBefore: 'let Component = styled.div`', 2337 | codeAfter: '`;', 2338 | after: '${props => `color: red`}', 2339 | semicolon: true, 2340 | }); 2341 | expect(firstComponent.nodes).toHaveLength(1); 2342 | expect(firstComponent.first.prop).toBe('color'); 2343 | expect(firstComponent.first.value).toBe('green'); 2344 | }); 2345 | 2346 | test('props with css helper (at the end)', () => { 2347 | let document = parse( 2348 | 'let Component = styled.div`color: green;${props => css`color: red`}`;', 2349 | ); 2350 | 2351 | expect(document.nodes).toHaveLength(2); 2352 | 2353 | let firstComponent = document.first; 2354 | 2355 | expect(firstComponent.raws).toEqual({ 2356 | isRuleLike: true, 2357 | styledSyntaxIsComponent: true, 2358 | styledSyntaxRangeStart: 27, 2359 | styledSyntaxRangeEnd: 67, 2360 | codeBefore: 'let Component = styled.div`', 2361 | codeAfter: '`;', 2362 | after: '${props => css`color: red`}', 2363 | semicolon: true, 2364 | }); 2365 | expect(firstComponent.nodes).toHaveLength(1); 2366 | expect(firstComponent.first.prop).toBe('color'); 2367 | expect(firstComponent.first.value).toBe('green'); 2368 | 2369 | let cssInterpolationFirst = firstComponent.next(); 2370 | 2371 | expect(cssInterpolationFirst.nodes).toHaveLength(1); 2372 | expect(cssInterpolationFirst.raws).toEqual({ 2373 | after: '', 2374 | semicolon: false, 2375 | isRuleLike: true, 2376 | styledOriginalContent: 'color: red', 2377 | styledSyntaxRangeEnd: 65, 2378 | styledSyntaxRangeStart: 55, 2379 | }); 2380 | 2381 | expect(cssInterpolationFirst.first.prop).toBe('color'); 2382 | expect(cssInterpolationFirst.first.value).toBe('red'); 2383 | expect(cssInterpolationFirst.first.raws.before).toBe(''); 2384 | }); 2385 | 2386 | test('props with no styled helpers (at the beginning)', () => { 2387 | let document = parse('let Component = styled.div`${props => `color: red`} color: green;`;'); 2388 | 2389 | expect(document.nodes).toHaveLength(1); 2390 | 2391 | let firstComponent = document.first; 2392 | 2393 | expect(firstComponent.raws).toEqual({ 2394 | isRuleLike: true, 2395 | styledSyntaxIsComponent: true, 2396 | styledSyntaxRangeStart: 27, 2397 | styledSyntaxRangeEnd: 65, 2398 | codeBefore: 'let Component = styled.div`', 2399 | codeAfter: '`;', 2400 | after: '', 2401 | semicolon: true, 2402 | }); 2403 | expect(firstComponent.nodes).toHaveLength(1); 2404 | expect(firstComponent.first.prop).toBe('color'); 2405 | expect(firstComponent.first.value).toBe('green'); 2406 | expect(firstComponent.first.raws.before).toBe('${props => `color: red`} '); 2407 | }); 2408 | 2409 | test('props with css helper (at the beginning)', () => { 2410 | let document = parse( 2411 | 'let Component = styled.div`${props => css`color: red`} color: green;`;', 2412 | ); 2413 | 2414 | expect(document.nodes).toHaveLength(2); 2415 | 2416 | let firstComponent = document.first; 2417 | 2418 | expect(firstComponent.raws).toEqual({ 2419 | isRuleLike: true, 2420 | styledSyntaxIsComponent: true, 2421 | styledSyntaxRangeStart: 27, 2422 | styledSyntaxRangeEnd: 68, 2423 | codeBefore: 'let Component = styled.div`', 2424 | codeAfter: '`;', 2425 | after: '', 2426 | semicolon: true, 2427 | }); 2428 | expect(firstComponent.nodes).toHaveLength(1); 2429 | expect(firstComponent.first.prop).toBe('color'); 2430 | expect(firstComponent.first.value).toBe('green'); 2431 | expect(firstComponent.first.raws.before).toBe('${props => css`color: red`} '); 2432 | 2433 | let cssInterpolation = firstComponent.next(); 2434 | 2435 | expect(cssInterpolation.nodes).toHaveLength(1); 2436 | expect(cssInterpolation.raws).toEqual({ 2437 | after: '', 2438 | semicolon: false, 2439 | isRuleLike: true, 2440 | styledOriginalContent: 'color: red', 2441 | styledSyntaxRangeEnd: 52, 2442 | styledSyntaxRangeStart: 42, 2443 | }); 2444 | 2445 | expect(cssInterpolation.first.prop).toBe('color'); 2446 | expect(cssInterpolation.first.value).toBe('red'); 2447 | expect(cssInterpolation.first.raws.before).toBe(''); 2448 | }); 2449 | }); 2450 | 2451 | describe('component notations', () => { 2452 | const notations = [ 2453 | 'styled.foo', 2454 | 'styled(Component)', 2455 | 'styled.foo.attrs({})', 2456 | 'styled(Component).attrs({})', 2457 | 'css', 2458 | 'createGlobalStyle', 2459 | ]; 2460 | 2461 | test.each(notations)('%s', (notation) => { 2462 | let document = parse('let Component = ' + notation + '`color: red;`;'); 2463 | 2464 | expect(document.nodes).toHaveLength(1); 2465 | 2466 | let component = document.first; 2467 | 2468 | expect(component.nodes).toHaveLength(1); 2469 | expect(component.first.prop).toBe('color'); 2470 | expect(component.first.value).toBe('red'); 2471 | expect(component.raws).toEqual({ 2472 | codeBefore: 'let Component = ' + notation + '`', 2473 | codeAfter: '`;', 2474 | after: '', 2475 | semicolon: true, 2476 | isRuleLike: true, 2477 | styledSyntaxIsComponent: true, 2478 | styledSyntaxRangeStart: 17 + notation.length, 2479 | styledSyntaxRangeEnd: 17 + notation.length + 11, 2480 | }); 2481 | }); 2482 | 2483 | test('passing a function', () => { 2484 | let document = parse('let Component = styled.div(props => `color: red;`);'); 2485 | 2486 | expect(document.nodes).toHaveLength(1); 2487 | 2488 | let component = document.first; 2489 | 2490 | expect(component.nodes).toHaveLength(1); 2491 | expect(component.first.prop).toBe('color'); 2492 | expect(component.first.value).toBe('red'); 2493 | expect(component.raws).toEqual({ 2494 | codeBefore: 'let Component = styled.div(props => `', 2495 | codeAfter: '`);', 2496 | after: '', 2497 | semicolon: true, 2498 | isRuleLike: true, 2499 | styledSyntaxIsComponent: true, 2500 | styledSyntaxRangeStart: 37, 2501 | styledSyntaxRangeEnd: 37 + 11, 2502 | }); 2503 | }); 2504 | }); 2505 | 2506 | test('supports TypeScript', () => { 2507 | let document = parse('let Component = styled.div<{ isDisabled?: boolean; }>`color: red;`;'); 2508 | 2509 | expect(document.nodes).toHaveLength(1); 2510 | expect(document.first.nodes).toHaveLength(1); 2511 | expect(document.first.first.prop).toBe('color'); 2512 | expect(document.first.first.value).toBe('red'); 2513 | expect(document.first.raws).toEqual({ 2514 | codeBefore: 'let Component = styled.div<{ isDisabled?: boolean; }>`', 2515 | codeAfter: '`;', 2516 | after: '', 2517 | semicolon: true, 2518 | isRuleLike: true, 2519 | styledSyntaxIsComponent: true, 2520 | styledSyntaxRangeEnd: 65, 2521 | styledSyntaxRangeStart: 54, 2522 | }); 2523 | expect(document.first.toString()).toBe('color: red;'); 2524 | }); 2525 | 2526 | test('component after a component with interpolation', () => { 2527 | let document = parse( 2528 | 'const Component = styled.div`\n${css`position: sticky;`}\n`;\nconst Trigger = styled.div``;', 2529 | ); 2530 | 2531 | expect(document.nodes).toHaveLength(3); 2532 | 2533 | expect(document.nodes[0].raws).toEqual({ 2534 | styledSyntaxRangeStart: 29, 2535 | styledSyntaxRangeEnd: 56, 2536 | after: '\n${css`position: sticky;`}\n', 2537 | codeBefore: 'const Component = styled.div`', 2538 | isRuleLike: true, 2539 | styledSyntaxIsComponent: true, 2540 | }); 2541 | 2542 | expect(document.nodes[1].raws).toEqual({ 2543 | styledSyntaxRangeStart: 36, 2544 | styledSyntaxRangeEnd: 53, 2545 | isRuleLike: true, 2546 | styledOriginalContent: 'position: sticky;', 2547 | semicolon: true, 2548 | after: '', 2549 | }); 2550 | 2551 | expect(document.nodes[2].raws).toEqual({ 2552 | styledSyntaxRangeStart: 86, 2553 | styledSyntaxRangeEnd: 86, 2554 | after: '', 2555 | codeBefore: '`;\nconst Trigger = styled.div`', 2556 | codeAfter: '`;', 2557 | isRuleLike: true, 2558 | styledSyntaxIsComponent: true, 2559 | }); 2560 | }); 2561 | 2562 | test('does not crash for invalid JavaScript syntax', () => { 2563 | let document = parse('const Component = styled.div`position: sticky'); 2564 | 2565 | expect(document.nodes).toHaveLength(0); 2566 | expect(document.toString()).toBe(''); 2567 | expect(document.raws).toEqual({}); 2568 | }); 2569 | --------------------------------------------------------------------------------