[
  {
    "path": ".cspell.json",
    "content": "{\n  \"ignorePaths\": [\n    \"node_modules/**\",\n    \".vscode/**\",\n    \"lib/**\"\n  ],\n  \"import\": [\n    \"@schoero/configs/cspell\"\n  ],\n  \"words\": [\n    \"astro\",\n    \"Atrule\",\n    \"autofix\",\n    \"callees\",\n    \"classcat\",\n    \"classnames\",\n    \"clsx\",\n    \"cnbuilder\",\n    \"csstree\",\n    \"daisyui\",\n    \"dcnb\",\n    \"DCNB\",\n    \"Declarators\",\n    \"ecma\",\n    \"eslintrc\",\n    \"espree\",\n    \"estree\",\n    \"jiti\",\n    \"linebreak\",\n    \"linebreakstyle\",\n    \"longhands\",\n    \"memfs\",\n    \"objstr\",\n    \"OBJSTR\",\n    \"oxfmt\",\n    \"quasis\",\n    \"shadcn\",\n    \"synckit\",\n    \"Tmpl\"\n  ]\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Create a report\nlabels: [\"bug\"]\nbody:\n  - attributes:\n      description: A clear and concise description of what the bug is.\n      label: Description\n      placeholder: Describe the bug here...\n    id: description\n    type: textarea\n    validations:\n      required: true\n  - attributes:\n      description: Which framework/flavor are you using?\n      label: Flavor\n      options:\n        - JSX\n        - TSX\n        - Svelte\n        - Vue\n        - Astro\n        - Angular\n        - HTML\n        - CSS\n        - JavaScript\n        - TypeScript\n    id: flavor\n    type: dropdown\n    validations:\n      required: true\n  - attributes:\n      description: The code that triggers the bug.\n      label: Code Input\n      placeholder: |\n        // Code snippet\n      render: tsx\n    id: code-input\n    type: textarea\n    validations:\n      required: true\n  - attributes:\n      description: What you expected to happen.\n      label: Expected Behavior\n      placeholder: |\n        // Expected output\n      render: tsx\n    id: expected-behavior\n    type: textarea\n    validations:\n      required: true\n  - attributes:\n      description: What actually happened.\n      label: Actual Behavior\n      placeholder: |\n        // Actual output\n      render: tsx\n    id: actual-behavior\n    type: textarea\n    validations:\n      required: true\n  - attributes:\n      description: Link to a reproduction (e.g. StackBlitz, CodeSandbox, GitHub repo).\n      label: Reproduction URL\n      placeholder: https://...\n    id: reproduction-url\n    type: input\n    validations:\n      required: false\n  - attributes:\n      description: Please paste the output of `npx eslint ./path/to/file` here.\n      label: ESLint Log\n      placeholder: |\n        // ESLint log output\n      render: shell\n    id: eslint-log\n    type: textarea\n    validations:\n      required: true\n  - attributes:\n      description: The relevant parts of your ESLint config\n      label: ESLint Config\n      render: typescript\n    id: eslint-config\n    type: textarea\n    validations:\n      required: true\n  - attributes:\n      description: Please list the versions of the tools you are using.\n      label: Versions\n      value: |\n        - ESLint: \n        - Parser: \n        - Plugin: \n        - Node: \n    id: versions\n    type: textarea\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.yml",
    "content": "name: Documentation\ndescription: Improvements or additions to documentation\nlabels: [\"documentation\"]\nbody:\n  - attributes:\n      description: Describe the documentation issue or improvement.\n      label: Documentation Issue\n    id: documentation\n    type: textarea\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Suggest an idea for this project\nlabels: [\"feature request\"]\nbody:\n  - attributes:\n      description: A clear and concise description of what the feature is.\n      label: Description\n      placeholder: Describe the feature here...\n    id: description\n    type: textarea\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.yml",
    "content": "name: Question\ndescription: Ask a question\nlabels: [\"question\"]\nbody:\n  - attributes:\n      description: What is your question?\n      label: Question\n    id: question\n    type: textarea\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches:\n      - main\n  push:\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          cache: npm\n          node-version: 24\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Lint\n        run: npm run lint:ci\n\n      - name: Typecheck\n        run: npm run typecheck\n\n      - name: Spellcheck\n        run: npm run spellcheck:ci\n\n  test:\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          cache: npm\n          node-version: ${{ matrix.node }}\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run build\n        run: npm run build:ci\n\n      - name: Install tailwindcss v3\n        run: npm run install:v3\n        \n      - name: Run tests with tailwindcss v3\n        run: npm run test:v3\n\n      - name: Run e2e tests with tailwindcss v3\n        run: npm run test:e2e\n\n      - name: Install tailwindcss v4\n        run: npm run install:v4\n\n      - name: Run tests with tailwindcss v4\n        run: npm run test:v4\n\n      - name: Run e2e tests with tailwindcss v4\n        run: npm run test:e2e\n\n    strategy:\n      fail-fast: true\n      matrix:\n        node:\n          - 20\n          - 22\n          - 24\n        os:\n          - ubuntu-latest\n          - windows-latest\n          - macos-latest\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ntmp\nlib\nlocal\n.DS_Store\n.env\n"
  },
  {
    "path": ".markdownlint.jsonc",
    "content": "{\n  \"extends\": \"@schoero/configs/markdownlint\"\n}"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"streetsidesoftware.code-spell-checker\",\n    \"davidanson.vscode-markdownlint\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"configurations\": [\n    {\n      \"args\": [\n        \"run\",\n        \"${relativeFileDirname}/${fileBasenameNoExtension}\"\n      ],\n      \"autoAttachChildProcesses\": true,\n      \"console\": \"integratedTerminal\",\n      \"name\": \"debug current test file\",\n      \"program\": \"${workspaceRoot}/node_modules/vitest/vitest.mjs\",\n      \"request\": \"launch\",\n      \"skipFiles\": [\"<node_internals>/**\", \"**/node_modules/**\"],\n      \"smartStep\": true,\n      \"type\": \"node\"\n    },\n    {\n      \"args\": [\n        \"run\",\n        \"${relativeFileDirname}/${fileBasenameNoExtension}\"\n      ],\n      \"autoAttachChildProcesses\": true,\n      \"console\": \"integratedTerminal\",\n      \"name\": \"debug current test file with node internals\",\n      \"program\": \"${workspaceRoot}/node_modules/vitest/vitest.mjs\",\n      \"request\": \"launch\",\n      \"skipFiles\": [],\n      \"smartStep\": true,\n      \"type\": \"node\"\n    }\n  ],\n  \"version\": \"0.2.0\"\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\n  // ESLint\n  \"[javascript][typescript][json][json5][jsonc][yaml]\": {\n    \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\"\n  },\n  \"eslint.nodePath\": \"node_modules/eslint\",\n  \"eslint.useFlatConfig\": true,\n  \"eslint.validate\": [\"javascript\", \"typescript\", \"json\", \"jsonc\", \"json5\", \"yaml\"],\n\n  \"eslint.rules.customizations\": [\n    { \"rule\": \"better-tailwindcss/*\", \"severity\": \"off\" }\n  ],\n  \"eslint.codeActionsOnSave.rules\": [\n    \"!better-tailwindcss/*\",\n    \"*\"\n  ],\n  \n  // tailwindcss\n  \"tailwindCSS.lint.cssConflict\": \"ignore\",\n  \"tailwindCSS.lint.suggestCanonicalClasses\": \"ignore\",\n\n  \"editor.formatOnSave\": false,\n\n  // Prettier\n  \"prettier.enable\": false,\n\n  // File nesting\n  \"explorer.fileNesting.enabled\": true,\n  \"explorer.fileNesting.expand\": false,\n  \"explorer.fileNesting.patterns\": {\n    \"*.ts\": \"$(capture).ts,$(capture).test.ts,$(capture).cts,$(capture).mts,$(capture).test.snap,$(capture).test-d.ts,$(capture).v4.ts,$(capture).async.ts,$(capture).v3.ts,$(capture).async.v4.ts,$(capture).async.v3.ts,$(capture).async.worker.ts,$(capture).async.worker.v4.ts,$(capture).async.worker.v3.ts\",\n    \"*.v3.ts\": \"$(capture).v4.ts\",\n    \"*.js\": \"$(capture).test.js,$(capture).cjs,$(capture).mjs,$(capture).d.ts,$(capture).d.ts.map,$(capture).js.map\"\n  },\n\n  // ES module import\n  \"typescript.preferences.importModuleSpecifier\": \"non-relative\",\n  \"typescript.preferences.importModuleSpecifierEnding\": \"js\",\n  \"typescript.preferences.useAliasesForRenames\": true,\n  \"typescript.preferences.autoImportFileExcludePatterns\": [\n    \"@types/node/test.d.ts\"\n  ],\n  \n  // Markdown\n  \"[markdown]\": {\n    \"editor.defaultFormatter\": \"DavidAnson.vscode-markdownlint\"\n  },\n\n  // VSCode\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\",\n    \"source.fixAll.markdownlint\": \"explicit\",\n    \"source.organizeImports\": \"never\"\n  },\n  \"editor.rulers\": [\n    119\n  ],\n  \"typescript.preferences.autoImportSpecifierExcludeRegexes\": [\"lib\"],\n  \"search.exclude\": {\n    \"lib\": true\n  },\n  \"typescript.tsdk\": \"node_modules/typescript/lib\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v4.5.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.4.1...v4.5.0)\n\n### Features\n\n- Add `ignore` option to `enforce-canonical-classes` ([#371](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/371))\n- Add `tabWidth` option to `enforce-consistent-line-wrapping` ([#367](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/367))\n\n### Fixes\n\n- Add missing logical classes ([#368](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/368))\n- Warning when tailwind css installation can't be found ([#373](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/373))\n- Only sort variants that are safe ([#370](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/370))\n\n## v4.4.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.4.0...v4.4.1)\n\n### Fixes\n\n- Remove auto detection of project root to set `cwd` ([#364](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/364))\n\n  If you're in a monorepo setup, you may need to [configure the `cwd`](https://github.com/schoero/eslint-plugin-better-tailwindcss?tab=readme-ov-file#monorepo-setup) manually.\n\n## v4.4.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.3.2...v4.4.0)\n\n### Features\n\n- Project root based cwd in monorepos ([#345](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/345))\n- Target specific arguments of callees ([#347](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/347))\n- New Anonymous functions matcher ([#348](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/348))\n- Add support for tag paths ([#354](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/354))\n- Reintroduce line ending and indentation misconfiguration warnings ([#351](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/351))\n- **worker:** Use SYNCKIT_TIMEOUT env var for timeout configuration ([#352](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/352))\n- Match default exports ([#346](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/346))\n- React twc preset ([#355](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/355))\n- Lint Template literal based on prefixed comments ([#356](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/356))\n- New rule `enforce-logical-properties` ([#358](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/358))\n- New rule `enforce-consistent-variant-order` ([#359](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/359))\n\n### Performance\n\n- Cache regex, early return ([#336](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/336))\n\n### Documentation\n\n- Add example to restrict unnamed groups ([#357](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/357))\n\n### ❤️ Contributors\n\n- Mickaël Depardon ([@squelix](https://github.com/squelix))\n- Mike Schutte ([@tmikeschu](https://github.com/tmikeschu))\n- Stephen Zhou ([@hyoban](https://github.com/hyoban))\n\n## v4.3.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.3.1...v4.3.2)\n\n### Fixes\n\n- **no-unnecessary-whitespace:** Preserve whitespaces in concatenated strings ([#339](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/339))\n- **enforce-consistent-class-order:** Non localized alphabetical sorting order ([#340](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/340))\n\n### Refactors\n\n- Lint concatenated strings ([#338](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/338))\n\n## v4.3.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.3.0...v4.3.1)\n\n### Fixes\n\n- Variable matchers leaking into function expressions ([#333](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/333))\n\n### Documentation\n\n- Add oxlint documentation ([#331](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/331))\n\n## v4.3.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.2.0...v4.3.0)\n\n### Features\n\n- Support curried calls ([#325](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/325))\n- Support callee paths ([#326](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/326))\n\n### Refactors\n\n- Simplify matcher config ([#324](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/324))\n  The matcher config has been simplified from a nested tuple structure to a simple array of objects. This makes it easier\n  to understand while also allowing better flexibility to support the new features. The old structure is still supported\n  for now, but will be removed in the next major version.\n\n  Check the updated [configuration documentation](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/configuration/advanced.md#selectors) for more information.\n\n## v4.2.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.1.1...v4.2.0)\n\n### Features\n\n- Add support for ESLint 10 ([#323](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/323))\n\n### Performance\n\n- Use shared worker to handle async calls ([#319](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/319))\n\n### ❤️ Contributors\n\n- Stephen Zhou ([@hyoban](https://github.com/hyoban))\n- Bjorn Antonissen ([@Bjornftw](https://github.com/Bjornftw))\n\n## v4.1.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.1.0...v4.1.1)\n\n### Fixes\n\n- Filter unrecommended rules ([#317](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/317))\n\n## v4.1.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.0.2...v4.1.0)\n\n### Features\n\n- Experimental css linting ([#314](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/314))\n- Add solid `classList` matcher ([#315](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/315))\n\n### Fixes\n\n- Type errors ([c3c9c40](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/c3c9c40))\n- Prevent linting when no literals are found ([51333c6](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/51333c6))\n- Add `exactOptionalPropertyTypes` to `tsconfig` ([#311](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/311))\n\n### ❤️ Contributors\n\n- Alexander Kachkaev ([@kachkaev](https://github.com/kachkaev))\n\n## v4.0.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.0.1...v4.0.2)\n\n### Fixes\n\n- `enforce-canonical-classes`: removal of unrelated classes ([#309](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/309))\n- `enforce-consistent-variable-syntax`: Support custom css functions ([#308](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/308))\n- Config types ([#310](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/310))\n\n## v4.0.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.0.0...v4.0.1)\n\n### Fixes\n\n- Disallow extra properties in rule options (valibot schemas) ([#295](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/295))\n- Configuration warnings getting lost ([#297](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/297))\n\n### ❤️ Contributors\n\n- Andrew Kazakov ([@andreww2012](https://github.com/andreww2012))\n\n## v4.0.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.8.0...v4.0.0)\n\nThis version includes a major rewrite of the internal architecture, improving performance and maintainability, resolving long-standing issues, and preparing the codebase for the future and for oxlint.\n\n### New Features\n\n- New rule: `enforce-canonical-classes` ([#232](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/232))\n- New options for `enforce-consistent-class-order` to sort \"component classes\" and \"unknown classes\" ([#263](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/263))\n  - `detectComponentClasses`: `boolean`\n  - `componentClassOrder`: `\"asc\" | \"desc\" | \"preserve\"`\n  - `componentClassPosition`: `\"start\" | \"end\"`\n  - `unknownClassOrder`: `\"asc\" | \"desc\" | \"preserve\"`\n  - `unknownClassPosition`: `\"start\" | \"end\"`\n- Added  `strictness: \"loose\"` option to `enforce-consistent-line-wrapping` to improve interoperability with prettier ([#260](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/260))\n- Better Performance\n- Oxlint support\n\n<br />\n\n### ⚠️ Breaking Changes\n\nFirst of all, the minimum required Node.js version is has changed to support v23.0.0, v22.12.0, v20.19.0 to support `require(esm)`\n\n- This made it possible to remove the `CommonJS` build ([#264](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/264))\n\n<br />\n\nSome rules have been renamed to better reflect their intentions:\n\n- Renamed rule `no-unregistered-classes` to `no-unknown-classes`\n- Renamed rule `sort-classes` to `enforce-consistent-class-order`\n- Renamed rule `multiline` to `enforce-consistent-line-wrapping`\n\nThe rule recommendations have been updated to enable new rules by default. Check the updated [rule recommendations](https://github.com/schoero/eslint-plugin-better-tailwindcss?tab=readme-ov-file#stylistic-rules) for more information.\n\n<br />\n\nFor some rules, the options have been renamed or changed:\n\n- Options for `better-tailwindcss/enforce-consistent-variable-syntax` have been renamed to `shorthand` and `variable`.\n- The default for `enforce-consistent-important-position` is now always `recommended`.\n- Renamed the `improved` sorting order for `enforce-consistent-class-order` to `strict` ([#245](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/245))\n  - `improved` is no longer the default option as most people expect the order to match the official order from tailwind.\n  - the `improved` order got renamed to `strict` to better describe its intentions.\n  - the logic of the `strict` order has changed:\n    - Classes that share the same base variants get grouped together.\n    - Classes with less variants come before classes with more variants.\n    - Classes with arbitrary variants come last.\n- The `enforce-consistent-line-wrapping` rule now groups variants more strictly. Previously it only grouped classes by their first variant. Now all variants are ordered correctly.\n\n<br />\n\nThe configs have been renamed and updated to match the recommended shape of ESLint.\n\n- Renamed configs ([#244](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/244))\n  - The following configs are now exposed:\n    - `recommended`\n    - `recommended-warn`\n    - `recommended-error`\n    - `stylistic`\n    - `stylistic-warn`\n    - `stylistic-error`\n    - `correctness`\n    - `correctness-warn`\n    - `correctness-error`\n    - `legacy-recommended`\n    - `legacy-recommended-warn`\n    - `legacy-recommended-error`\n    - `legacy-stylistic`\n    - `legacy-stylistic-warn`\n    - `legacy-stylistic-error`\n    - `legacy-correctness`\n    - `legacy-correctness-warn`\n    - `legacy-correctness-error`\n  - Please check the updated [Parser Documentation](https://github.com/schoero/eslint-plugin-better-tailwindcss?tab=readme-ov-file#quick-start) to see the recommended way to set up the plugin with your parser.\n\n<br />\n\nOther changes:\n\n- Function `getDefaultIgnoredUnregisteredClasses()` has been removed.\n- Removed rule regex matchers\n- Preserve normal quotes whenever possible ([#246](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/246))\n\nHere is the full list of changes in this version:\n\n### Features\n\n- New rule: `enforce-canonical-classes` ([#232](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/232))\n- Oxlint support ([#284](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/284)\n- Add `strictness: \"loose\"` option to `enforce-consistent-line-wrapping` ([#260](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/260))\n- Add settings option to configure `messageStyle` ([#276](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/276))\n- **angular:** Support bound attribute classes ([#277](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/277))\n- **svelte:** Support class directive ([#278](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/278))\n\n### Fixes\n\n- Don't match attribute values for bound attribute names ([#291](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/291))\n- Correctly override shared settings with rule options ([#289](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/289))\n- Invalid variant grouping order ([#282](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/282))\n- Ignore variants in custom component classes ([#258](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/258))\n- Angular line wrapping ([#259](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/259))\n\n### Refactors\n\n- Deprecate `/api/` path for imports ([#281](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/281))\n- Update rule recommendations ([#280](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/280))\n\n### Documentation\n\n- Add `detectComponentClasses` to settings ([388103e](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/388103e))\n- Add attribute matcher example ([#272](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/272))\n- Improve configuration guide ([bd873ea](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/bd873ea))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️  Ignore indexed access keys ([#292](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/292))\n- ⚠️  Update rule recommendations ([#280](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/280))\n- ⚠️  Remove separate `CommonJS` build ([#264](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/264))\n      Minimum Node.js version to v23.0.0, v22.12.0, v20.19.0 to support `require(esm)`\n- ⚠️  Preserve normal quotes whenever possible ([#246](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/246))\n- ⚠️  Renamed the `improved` sorting order to `strict` ([#245](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/245))\n- ⚠️  Rename configs ([#244](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/244))\n- ⚠️  Renamed rule `no-unregistered-classes` to `no-unknown-classes`\n- ⚠️  Renamed rule `sort-classes` to `enforce-consistent-class-order`\n- ⚠️  Renamed rule `multiline` to `enforce-consistent-line-wrapping`\n- ⚠️  Options for `better-tailwindcss/enforce-consistent-variable-syntax` have been renamed to `shorthand` and `variable`.\n- ⚠️  Function `getDefaultIgnoredUnregisteredClasses()` has been removed.\n- ⚠️  The default for `enforce-consistent-important-position` is now always `recommended`. If you are on tailwindcss v3 need to manually set it to `legacy` to keep it working for tailwindcss v3.\n- ⚠️  Removed rule regex matchers\n\n### ❤️ Contributors\n\n- V-iktor ([@V-iktor](https://github.com/V-iktor))\n\n## v3.8.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.11...v3.8.0)\n\n### Features\n\n- **no-unregistered-classes:** Support `@import layer(components)` ([#257](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/257))\n\n### Fixes\n\n- Wrong documentation url ([#255](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/255))\n- Ignore variants in custom component classes ([#258](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/258))\n- Angular line wrapping ([#259](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/259))\n\n### ❤️ Contributors\n\n- Carlos Marques <karkosyk@gmail.com>\n\n## v3.7.11\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v4.0.0-beta.3...v3.7.11)\n\n### Fixes\n\n- Convert missing flex shrink and grow utilities ([#236](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/236))\n- Ignore literals in binary expressions ([#238](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/238))\n- Allow interpolations in normal svelte string literals ([#239](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/239))\n- Only show config warning when config is set and not found ([#240](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/240))\n\n### ❤️ Contributors\n\n- Akameco ([@akameco](https://github.com/akameco))\n\n## v3.7.10\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.9...v3.7.10)\n\n### Fixes\n\n- `enforce-shorthand-classes` to include horizontal and vertical cases for `rounded` classes ([#231](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/231))\n\n### Chore\n\n- Correct recommended rules to match implementation ([#229](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/229))\n\n### ❤️ Contributors\n\n- Andrew Kodkod ([@akodkod](https://github.com/akodkod))\n- 2754 ([@2754github](https://github.com/2754github))\n\n## v3.7.9\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.8...v3.7.9)\n\n### Fixes\n\n- Don't match index accessed object keys ([#227](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/227))\n\n## v3.7.8\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.7...v3.7.8)\n\n### Fixes\n\n- Improved angular support ([#182](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/182))\n  - Fixes object key detection for intersecting classes\n  - Adds support for `pathPattern` in angular\n\n## v3.7.7\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.6...v3.7.7)\n\n### Fixes\n\n- Compound variants with slots class string not being detected ([#219](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/219))\n\n### ❤️ Contributors\n\n- tim-spitzer-syzygy ([@tim-spitzer-syzygy](https://github.com/tim-spitzer-syzygy))\n\n## v3.7.6\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.5...v3.7.6)\n\n### Fixes\n\n- Check for tailwindcss before running rules ([#217](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/217))\n- Angular: Prevent crash when objectContent is undefined in createLiteralByLiteralMapKey ([#215](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/215))\n\n### Tests\n\n- Add no-unregistered-classes test for DaisyUI classes ([#186](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/186))\n\n### ❤️ Contributors\n\n- Paul Parker ([@pauldesmondparker](https://github.com/pauldesmondparker))\n- Yossi Yedid ([@yossiyedid](https://github.com/yossiyedid))\n\n## v3.7.5\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.4...v3.7.5)\n\n### Fixes\n\n- Matching object values with immediate indexed access ([#212](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/212))\n\n## v3.7.4\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.3...v3.7.4)\n\n### Fixes\n\n- Error in no-conflicting-classes when used in tailwindcss 3 ([#205](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/205))\n- Invalid config warning when config was actually found ([#206](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/206))\n- Differentiate shorthands for the same classes with different variants ([#207](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/207))\n\n## v3.7.3\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.2...v3.7.3)\n\n### Fixes\n\n- Invalid fix for multiple vars in `enforce-consistent-variable-syntax` ([#200](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/200))\n\n## v3.7.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.1...v3.7.2)\n\n### Fixes\n\n- Error when no tsconfig is available ([#195](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/195))\n\n### Refactors\n\n- Refine cache ([#196](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/196))\n\n## v3.7.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.7.0...v3.7.1)\n\n### Fixes\n\n- `no-unnecessary-whitespace` false positive on empty string ([#191](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/191))\n- Don't convert variable definitions ([#192](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/192))\n\n### Chore\n\n- Update dependencies ([#193](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/193))\n\n## v3.7.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.6.3...v3.7.0)\n\n### Features\n\n- Support tsconfig paths ([#185](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/185))\n\n### Refactors\n\n- Exact unnecessary whitespace fixes ([#184](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/184))\n\n## v3.6.3\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.6.2...v3.6.3)\n\n### Fixes\n\n- Error position ([7b699ee](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/7b699ee))\n\n### Refactors\n\n- Add missing deprecations ([#181](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/181))\n- Variable syntax tailwindcss3 shorthand ([#183](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/183))\n\n## v3.6.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.6.1...v3.6.2)\n\n### Fixes\n\n- Fixes crash when importing css files via tsconfig path alias and [`detectComponentClasses`](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-unregistered-classes.md#detectcomponentclasses) enabled ([#178](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/178))\n- Fixes component classes not getting updated when inside an imported file ([#178](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/178))\n- Disallow extra properties in rule options ([#180](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/180))\n\n### ❤️ Contributors\n\n- Andrew Kazakov ([@andreww2012](https://github.com/andreww2012))\n\n## v3.6.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.6.0...v3.6.1)\n\n### Fixes\n\n- Recursively reading imports ([#175](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/175))\n\n## v3.6.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.5.2...v3.6.0)\n\n### Features\n\n- New rule `enforce-consistent-important-position` ([#167](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/167))\n- New rule `no-deprecated-classes` ([#169](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/169))\n\n### Fixes\n\n- Support starting important in `enforce-shorthand-classes` ([#164](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/164))\n- Error position ([a55a6cc](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/a55a6cc))\n\n## v3.5.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.5.1...v3.5.2)\n\n### Fixes\n\n- Tailwind 3 shorthand classes with important modifier ([#162](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/162))\n\n## v3.5.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.5.0...v3.5.1)\n\n### Fixes\n\n- False reports of shorthand classes ([c5f14ab](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/c5f14ab))\n\n## v3.5.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.4.4...v3.5.0)\n\n### Features\n\n- New Rule: Enforce shorthand classes ([#153](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/153))\n\n### Fixes\n\n- Bump tailwindcss peer dependency ([#157](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/157))\n- Regex deprecation warning ([#161](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/161))\n\n## v3.4.4\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.4.3...v3.4.4)\n\n### Fixes\n\n- Altering variant order in tailwindcss cache ([#151](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/151))\n\n### Documentation\n\n- Add example for arbitrary values ([ef6faa2](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/ef6faa2))\n\n## v3.4.3\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.4.2...v3.4.3)\n\n### Fixes\n\n- Prevent removal of whitespace between template literals ([#147](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/147))\n- Extract class variants via tailwind ([#146](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/146))\n\n## v3.4.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.4.1...v3.4.2)\n\n### Fixes\n\n- Template literals resulting in `undefined` path in getESObjectPath causing false positives ([#142](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/142))\n\n### ❤️ Contributors\n\n- Long Zheng ([@longzheng](https://github.com/longzheng))\n\n## v3.4.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.4.0...v3.4.1)\n\n### Fixes\n\n- Detect conflicts with multiple properties ([#137](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/137))\n\n## v3.4.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.3.1...v3.4.0)\n\n### Features\n\n- Add customizable autofix option to `no-restricted-classes` ([#133](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/133))\n\n### Refactors\n\n- Rename rules for better consistency ([#134](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/134))\n\n  - better-tailwindcss/multiline -> better-tailwindcss/enforce-consistent-line-wrapping\n  - better-tailwindcss/sort-classes -> better-tailwindcss/enforce-consistent-class-order\n\n  The old names will still work for now, but will be removed in the next major version.\n\n## v3.3.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.3.0...v3.3.1)\n\n### Fixes\n\n- Prevent variable matchers from crossing arrow function boundaries ([#131](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/131))\n- Sorting order with unregistered class with variant ([#132](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/132))\n\n## v3.3.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.2.1...v3.3.0)\n\n### Features\n\n- No-restricted-classes rule to support custom error messages ([#129](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/129))\n\n### Fixes\n\n- Node version range ([b50df13](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/b50df13))\n\n## v3.2.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.2.0...v3.2.1)\n\n### Fixes\n\n- Don't report inside member expressions ([#120](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/120))\n\n## v3.2.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.1.0...v3.2.0)\n\n### Features\n\n- Auto detect custom component layer classes ([#111](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/111))\n- Ignore prefix in groups ([#110](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/110))\n- Support prefixed groups and tags ([#115](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/115))\n\n### Fixes\n\n- Add additional tailwind variants matchers ([#116](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/116))\n\n## v3.1.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v3.0.0...v3.1.0)\n\n### Features\n\n- Add support for astro syntactic sugar ([#103](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/103))\n- New rule `enforce consistent variable syntax` ([#101](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/101))\n\n### Fixes\n\n- Remove `name` property ([#105](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/105))\n\n## v3.0.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v2.1.2...v3.0.0)\n\nThis version adds 3 new correctness rules to the plugin. To better reflect the new scope of the plugin it was renamed from `eslint-plugin-readable-tailwind` to `eslint-plugin-better-tailwindcss`. <https://github.com/schoero/eslint-plugin-readable-tailwind/issues/86#issuecomment-2855845766>\n\nThe predefined configs also have been renamed to better reflect their scope.\n\n### Features\n\n- [no-unregistered-classes](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-unregistered-classes.md): Report classes not registered with tailwindcss. ([#89](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/89))\n- [no-conflicting-classes](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-conflicting-classes.md): Report classes that produce conflicting styles. ([#90](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/90))\n- [no-restricted-classes](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-restricted-classes.md): Disallow restricted classes. ([#92](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/92))\n\n#### ⚠️ Breaking changes\n\n- Plugin renamed to `eslint-plugin-better-tailwindcss`\n- Deprecate [Regex matchers](https://github.com/schoero/eslint-plugin-readable-tailwind/blob/v2.1.2/docs/concepts/concepts.md#regular-expressions) to simplify the configuration. ([#98](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/98))  \n  [Regex matchers](https://github.com/schoero/eslint-plugin-readable-tailwind/blob/v2.1.2/docs/concepts/concepts.md#regular-expressions) were an early attempt to make the plugin more flexible. However, they were quickly replaced with [Matchers](https://github.com/schoero/eslint-plugin-readable-tailwind/blob/v2.1.2/docs/concepts/concepts.md#matchers) which work on the Abstract Syntax Tree and are far more powerful. Support for [Regex matchers](https://github.com/schoero/eslint-plugin-readable-tailwind/blob/v2.1.2/docs/concepts/concepts.md#regular-expressions) will be removed in the next major version.  \n\n- `warning` and `error` configs have been removed. Use `recommended-warn` or `recommended-error` instead. ([#99](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/99))\n\n### Migration\n\n1. Replace `eslint-plugin-readable-tailwind` with `eslint-plugin-better-tailwindcss`:\n\n  ```sh\n  npm uninstall eslint-plugin-readable-tailwind\n  ```\n\n  ```sh\n  npm i -D eslint-plugin-better-tailwindcss\n  ```\n\n1. Update the imports in your config:\n\n  ```diff\n  - import eslintPluginReadableTailwind from \"eslint-plugin-readable-tailwind\"; \n  + import eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\n  ```\n\n1. Migrate to the new configs\n\n  ```diff\n  rules: {\n       // enable all recommended rules to warn\n  -    ...eslintPluginReadableTailwind.configs.warning.rules,\n  +   ...eslintPluginBetterTailwindcss.configs[\"recommended-warn\"].rules,\n       // enable all recommended rules to error\n  -    ...eslintPluginReadableTailwind.configs.error.rules,\n  +    ...eslintPluginBetterTailwindcss.configs[\"recommended-error\"].rules,\n\n      // or configure rules individually\n  -    \"readable-tailwind/multiline\": [\"warn\", { printWidth: 100 }]\n  +    \"better-tailwindcss/multiline\": [\"warn\", { printWidth: 100 }] \n    }\n  ```\n\n## v2.1.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-readable-tailwind/compare/v2.1.1...v2.1.2)\n\n### Fixes\n\n- Multiline quotes ([#96](https://github.com/schoero/eslint-plugin-readable-tailwind/pull/96))\n\n### Refactors\n\n- Report error for each duplicate class instead of the whole class string ([#91](https://github.com/schoero/eslint-plugin-readable-tailwind/pull/91))\n\n## v2.1.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v2.1.0...v2.1.1)\n\n### Fixes\n\n- Unnecessarily escaped quotes in autofixed classes ([#88](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/88))\n\n## v2.1.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v2.0.1...v2.1.0)\n\n### Features\n\n- Experimental angular support. ([#85](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/85))\n\n### Fixes\n\n- Keep carriage return in es literals when used with vue parser ([#84](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/84))\n\n## v2.0.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v2.0.0...v2.0.1)\n\n### Fixes\n\n- Keep original newline characters ([a564783](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/a564783))\n\n### Refactors\n\n- Display warning if plugin is misconfigured ([7c532cd](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/7c532cd))\n\n### Documentation\n\n- Update quick start guide ([e570981](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/e570981))\n\n## v2.0.0\n\nAdds tailwindcss v4 support while keeping support for tailwindcss v3. ([#78](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/78))\n\nThis version contains breaking changes. Most notably support for Node.js < 20 had to be dropped. The other breaking changes are mostly just changes of the default config, that may cause linting errors.\n\n### Migration\n\n- If you use tailwindcss v4, you should specify the [`entryPoint`](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/sort-classes.md#entrypoint) of the css based tailwind configuration file for the [sort-classes](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/sort-classes.md) rule or in the [settings](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/settings/settings.md#entrypoint).\n- If you have customized the `classAttributes` option for any of the rules or via the [settings](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/settings/settings.md#attributes), rename the option to [`attributes`](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/settings/settings.md#attributes)\n- If you have customized `attributes`,  `callees`, `variables`,  or `tags`, escape any reserved characters for regular expressions in the name as the name is now evaluated as a regular expression.\n\n  For example:\n\n  ```diff\n   {\n     variables: [\n  -    \"$MyVariable\"\n  +    \"\\\\$MyVariable\"\n     ]\n   }\n  ```\n\n### Changes\n\n- Reload tailwind config automatically if a change is detected.\n- Options now correctly override settings ([#66](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/66))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️  Drop support for Node.js < 20 due to incompatibility of worker threads.\n- ⚠️  Add support for tailwindcss v4 ([#25](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/25))\n  - The official class ordering seems to have changed slightly.\n  - The `improved` sorting order will no longer sort variants alphabetically, instead it just makes sure that identical variants are grouped together.\n  \n- ⚠️  Regex names ([#63](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/63))\n  - [\"Names\"](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/configuration/advanced.md#name-based-matching) can now be regular expressions. This is a breaking change, if you have names configured that contain reserved characters in regular expressions like `$`.\n- ⚠️  Enable `no-duplicate-classes` by default ([#67](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/67))\n- ⚠️  Change default  `multiline` grouping to `newLine` ([#68](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/68))\n- ⚠️  Rename `classAttributes` to `attributes` ([#69](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/69))\n\n## v1.9.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v2.0.0-beta.2...v1.9.1)\n\n### Fixes\n\n- Lint `className` in render functions inside object ([#75](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/75))\n\n## v1.9.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.8.2...v1.9.0)\n\n### Features\n\n- Template literal tags ([#65](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/65))\n\n## v1.8.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.8.1...v1.8.2)\n\n### Fixes\n\n- Fixing loop when lines wrap on two lines immediately but was theoretically short enough to not wrap ([#61](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/61))\n\n## v1.8.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.8.0...v1.8.1)\n\n### Refactors\n\n- Improve display of linting errors ([#60](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/60))\n\n## v1.8.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.7.0...v1.8.0)\n\n### Features\n\n- Add support to globally configure shared options across all rules via the settings object ([#56](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/56))\n\n## v1.7.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.6.1...v1.7.0)\n\n### Features\n\n- New option `preferSingleLine` ([#54](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/54))\n\n## v1.6.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.6.0...v1.6.1)\n\n### Fixes\n\n- Group type `never` not working with expressions ([#53](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/53))\n\n## v1.6.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.5.3...v1.6.0)\n\n### Features\n\n- New rule `no-duplicate-classes` ([#49](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/49))  \n  This rule will be enabled by default in v2.0.0. If you want to enable it now, please refer to the [rule documentation](https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-duplicate-classes.md).  \n  You can suggest additional rules in the [discussions](https://github.com/schoero/eslint-plugin-better-tailwindcss/discussions/categories/new-rules-or-options?discussions_q=category%3A%22New+rules+or+options%22+).  \n\n### Refactors\n\n- Revert back to vitest ([38f6eab](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/38f6eab))\n\n## v1.5.3\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.5.2...v1.5.3)\n\n### Refactors\n\n- Insertion of unnecessary escape characters ([#47](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/47))\n\n## v1.5.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.5.1...v1.5.2)\n\n### Fixes\n\n- Remove unnecessary plugin import in shared config ([#44](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/44))\n- Support svelte shorthand syntax ([#43](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/43))\n\n## v1.5.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.5.0...v1.5.1)\n\n### Fixes\n\n- Commonjs build ([#39](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/39))\n\n## v1.5.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.4.0...v1.5.0)\n\n### Features\n\n- Vue bound classes ([#31](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/31))\n\n### Fixes\n\n- Change quotes in multiline arrays ([#32](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/32))\n- Escape nested quotes ([#33](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/33))\n- Allow call expressions as object values ([#34](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/34))\n- Attributes are no longer case sensitive ([#35](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/35))\n- Warn in html matchers ([#36](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/36))\n- Don't treat escape characters as whitespace ([6aa74f8](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/6aa74f8))\n\n### Refactors\n\n- Simplify build system ([#26](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/26), [#29](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/29))\n\n## v1.4.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.3.2...v1.4.0)\n\n### Features\n\n- Matchers ([#28](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/28))\n\n## v1.3.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.3.1...v1.3.2)\n\n### Fixes\n\n- Remove unnecessary newline after single sticky class ([#23](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/23))\n- Prevent inserting new line if the first class is already too long ([#24](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/24))\n\n### Tests\n\n- Simplify testing ([#22](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/22))\n\n## v1.3.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.3.0...v1.3.1)\n\n### Fixes\n\n- Accept tabs ([#21](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/21))\n\n## v1.3.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.2.5...v1.3.0)\n\n### Features\n\n- Add eslint 9 support ([#19](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/19))\n\n### Chore\n\n- Update dependencies ([be69b11](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/be69b11))\n\n## v1.2.5\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.2.4...v1.2.5)\n\n### Performance\n\n- Cache tailwind config and context ([#16](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/16))\n\n### Fixes\n\n- Resolving tailwind config ([#15](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/15))\n\n## v1.2.4\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.2.3...v1.2.4)\n\n### Fixes\n\n- Sticky expressions ([#13](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/13))\n\n## v1.2.3\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.2.2...v1.2.3)\n\n### Fixes\n\n- Remove unnecessary trailing spaces in multiline strings ([#12](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/12))\n- False positives when using `crlf` ([#11](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/11))\n\n## v1.2.2\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.2.1...v1.2.2)\n\n### Fixes\n\n- False positives of unnecessary whitespace around template literal elements ([#9](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/9))\n\n## v1.2.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.2.0...v1.2.1)\n\n### Fixes\n\n- Don't wrap empty attributes ([#8](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/8))\n\n## v1.2.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.1.1...v1.2.0)\n\n### Features\n\n- Lint variables ([#7](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/7))\n\n### Fixes\n\n- Apply nested regex only to container groups ([#6](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/6))\n\n## v1.1.1\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.1.0...v1.1.1)\n\n### Fixes\n\n- Invalid collapsing with template literal expressions ([adfafbf](https://github.com/schoero/eslint-plugin-better-tailwindcss/commit/adfafbf))\n\n## v1.1.0\n\n[compare changes](https://github.com/schoero/eslint-plugin-better-tailwindcss/compare/v1.0.0...v1.1.0)\n\n### Features\n\n- Collapse unnecessary newlines ([#4](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/4))\n- Regex as callees ([#3](https://github.com/schoero/eslint-plugin-better-tailwindcss/pull/3))\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to eslint-plugin-better-tailwindcss\n\nFirst off, thank you for considering contributing to **eslint-plugin-better-tailwindcss**! Your help is essential to making this project better. This document provides guidelines and instructions for contributing.\n\n<br />\n\n## How Can I Contribute?\n\n### Reporting Bugs\n\nIf you find a bug, please create an issue on [GitHub Issues](https://github.com/schoero/eslint-plugin-better-tailwindcss/issues) with:\n\n- **Clear title and description** of the issue\n- **Steps to reproduce** the problem\n- **Expected behavior** vs **actual behavior**\n- **Your environment**: Node.js version, npm version, ESLint version, Tailwind CSS version\n- **Code examples** or screenshots if applicable\n\n<br />\n\n### Feature Requests\n\nFeature requests are tracked as GitHub issues. When creating a feature request, include:\n\n- **Clear title and description**\n- **Use case and benefits** of the proposed feature\n- **Possible implementation** approach (if you have ideas)\n- **Examples** of how the feature would be used\n\n<br />\n\n### Pull Requests\n\nPull requests are appreciated! Here's the process:\n\n#### Prerequisites\n\n- Node.js `^20.11.0` or `>=21.2.0`\n- npm `>=8.0.0`\n\n<br />\n\n##### Installation\n\n```bash\ngit clone https://github.com/schoero/eslint-plugin-better-tailwindcss.git\ncd eslint-plugin-better-tailwindcss\nnpm install\n```\n\n<br />\n\n##### Fork the repository and create your branch from `main`\n\n   ```bash\n   git checkout -b feat/your-feature-name\n   ```\n\n<br />\n\n##### Set up the development environment\n\n   ```bash\n   npm install\n   ```\n\n<br />\n\n##### Make your changes\n\n- Follow the project's code style\n- Add tests for new features or bug fixes\n- Update documentation if needed\n- Keep commits atomic and write clear commit messages\n\n<br />\n\n##### Run the test suite\n\n   ```bash\n   npm test\n   ```\n\n   If you use vscode, you can open a test file and press <kbd>F5</kbd> to run all tests in the file in debug mode.  \n\n   The plugin supports both Tailwind CSS v3 and v4. Use the following commands to test against both versions:  \n\n   ```bash\n   npm run install:v3\n   npm run test:v3\n   npm run install:v4\n   npm run test:v4\n   ```\n\n<br />\n\n##### Fix linting and formatting issues\n\n   ```bash\n   npm run lint:fix\n   ```\n\n<br />\n\n##### Check type validity\n\n   ```bash\n   npm run typecheck\n   ```\n\n<br />\n\n##### Build the project\n\n   ```bash\n   npm run build\n   ```\n\n<br />\n\n##### Commit and push your changes\n\n```bash\ngit add .\ngit commit -m \"feat: add new feature\" # or \"fix: fix issue\", \"docs: update docs\", etc.\ngit push origin feat/your-feature-name\n```\n\nUse conventional commit messages:\n\n- `feat:` for new features\n- `fix:` for bug fixes\n- `docs:` for documentation changes\n- `test:` for test changes\n- `refactor:` for code refactoring\n- `chore:` for maintenance tasks\n- `ci:` for CI/CD changes\n\nExample: `feat: add new rule for enforcing class ordering`\n\n<br />\n\n##### Create a pull request on GitHub with\n\n- Clear title and description\n- Reference to related issues\n- Summary of changes\n\n<br />\n\n### Test Template\n\nThe project includes a sophisticated testing abstraction that automatically tests rules across multiple parsers (Angular, Astro, HTML, JSX, Svelte, and Vue). Use the `lint()` helper from `tests/utils/lint.ts`:\n\n```ts\nimport { describe, it } from \"vitest\";\n\nimport { yourRule } from \"better-tailwindcss:rules/your-rule.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\ndescribe(yourRule.name, () => {\n  it(\"should describe what it does\", () => {\n    lint(yourRule, {\n      invalid: [\n        {\n          angular: '<img class=\"bad classes\" />',\n          angularOutput: '<img class=\"fixed classes\" />',\n          html: '<img class=\"bad classes\" />',\n          htmlOutput: '<img class=\"fixed classes\" />',\n          jsx: '() => <img class=\"bad classes\" />',\n          jsxOutput: '() => <img class=\"fixed classes\" />',\n          svelte: '<img class=\"bad classes\" />',\n          svelteOutput: '<img class=\"fixed classes\" />',\n          vue: '<template><img class=\"bad classes\" /></template>',\n          vueOutput: '<template><img class=\"fixed classes\" /></template>',\n\n          errors: 1,\n          options: [{ /* rule options */ }]\n        }\n      ],\n      valid: [\n        {\n          angular: '<img class=\"valid classes\" />',\n          html: '<img class=\"valid classes\" />',\n          jsx: '() => <img class=\"valid classes\" />',\n          svelte: '<img class=\"valid classes\" />',\n          vue: '<template><img class=\"valid classes\" /></template>',\n\n          options: [{ /* rule options */ }]\n        }\n      ]\n    });\n  });\n});\n```\n\n<br />\n\n## Recognition\n\nContributors will be recognized in:\n\n- Pull request acknowledgments\n- CHANGELOG entries\n- GitHub contributors list\n\nThank you for your contributions!\n"
  },
  {
    "path": "FUNDING.yml",
    "content": "github:\n  - schoero\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Roger Schönbächler\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/eslint-plugin-better-tailwindcss-logo-dark.svg\">\n    <source media=\"(prefers-color-scheme: light)\" srcset=\"./assets/eslint-plugin-better-tailwindcss-logo-light.svg\">\n    <img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"./assets/eslint-plugin-better-tailwindcss-logo.svg\">\n  </picture>\n</div>\n\n<h1 align=\"center\">eslint-plugin-better-tailwindcss</h1>\n\n<br/>\n<br/>\n\n<div align=\"center\">\n  <a alt=\"GitHub license\" href=\"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/LICENSE\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/github/license/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=32363B&color=ffffff\" /><source media=\"(prefers-color-scheme: light)\" srcset=\"https://img.shields.io/github/license/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /><img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"https://img.shields.io/github/license/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /></picture></a>\n  <a alt=\"npm version\" href=\"https://www.npmjs.com/package/eslint-plugin-better-tailwindcss?activeTab=versions\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/npm/v/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=32363B&color=ffffff\" /><source media=\"(prefers-color-scheme: light)\" srcset=\"https://img.shields.io/npm/v/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /><img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"https://img.shields.io/npm/v/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /></picture></a>\n  <a alt=\"GitHub issues\" href=\"https://github.com/schoero/eslint-plugin-better-tailwindcss/issues\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/github/issues/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=32363B&color=ffffff\" /><source media=\"(prefers-color-scheme: light)\" srcset=\"https://img.shields.io/github/issues/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /><img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"https://img.shields.io/github/issues/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /></picture></a>\n  <a alt=\"npm total downloads\" href=\"https://www.npmjs.com/package/eslint-plugin-better-tailwindcss?activeTab=readme\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/npm/dt/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=32363B&color=ffffff\" /><source media=\"(prefers-color-scheme: light)\" srcset=\"https://img.shields.io/npm/dt/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /><img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"https://img.shields.io/npm/dt/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /></picture></a>\n  <a alt=\"GitHub repo stars\" href=\"https://github.com/schoero/eslint-plugin-better-tailwindcss/stargazers\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/github/stars/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=32363B&color=ffffff\" /><source media=\"(prefers-color-scheme: light)\" srcset=\"https://img.shields.io/github/stars/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\" /><img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"https://img.shields.io/github/stars/schoero/eslint-plugin-better-tailwindcss?style=flat-square&labelColor=EBEEF2&color=000000\"/></picture></a>\n  <a alt=\"GitHub sponsors\" href=\"https://github.com/sponsors/schoero\"><picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"https://img.shields.io/github/sponsors/schoero?style=flat-square&labelColor=32363B&color=ffffff\" /><source media=\"(prefers-color-scheme: light)\" srcset=\"hhttps://img.shields.io/github/sponsors/schoero?style=flat-square&labelColor=EBEEF2&color=000000\" /><img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"https://img.shields.io/github/sponsors/schoero?style=flat-square&labelColor=EBEEF2&color=000000\" /></picture></a>\n</div>\n\n<br/>\n<br/>\n\nESLint/Oxlint plugin with formatting and linting rules to help you write cleaner, more maintainable Tailwind CSS.  \n\nThe formatting rules focus on improving readability by automatically breaking up long Tailwind class strings into multiple lines and sorting/grouping them in a logical order. The linting rules enforce best practices and catch potential issues, ensuring that you're writing valid Tailwind CSS.  \n\nThis plugin supports a wide range of projects, including React, Solid.js, Qwik, Svelte, Vue, Astro, Angular, HTML or plain JavaScript or TypeScript.  \n\n<br/>\n<br/>\n\n<div align=\"center\">\n  <img alt=\"eslint-plugin-better-tailwindcss example\" width=\"640px\" src=\"./assets/eslint-plugin-better-tailwindcss-demo.png\">\n</div>\n\n<br/>\n<br/>\n\n<div align=\"center\">\n\n  <a href=\"https://github.com/sponsors/schoero\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/sponsor-dark.svg\">\n      <source media=\"(prefers-color-scheme: light)\" srcset=\"./assets/sponsor-light.svg\">\n      <img alt=\"eslint-plugin-better-tailwindcss logo\" src=\"./assets/sponsor-light.svg\">\n    </picture>\n  </a>\n  \n  <br/>\n  <br/>\n  \n  [Buy me a coffee](https://buymeacoffee.com/schoero) | [GitHub Sponsors](https://github.com/sponsors/schoero)\n  \n  Help support this project.  \n  If you or your company benefit from this project, please consider becoming a sponsor or making a one-time donation.  \n  Your contribution will help me to maintain and develop the project.\n\n</div>\n\n<br/>\n<br/>\n\n## Installation\n\n```sh\nnpm i -D eslint-plugin-better-tailwindcss\n```\n\n<br/>\n\n## Quick start\n\nDepending on the flavor you are using, you need to install and configure the corresponding parser:\n\n- React: [.jsx](docs/parsers/jsx.md) · [.tsx](docs/parsers/tsx.md)  \n- SolidJS: [.jsx](docs/parsers/jsx.md) · [.tsx](docs/parsers/tsx.md)  \n- Qwik: [.jsx](docs/parsers/jsx.md) · [.tsx](docs/parsers/tsx.md)  \n- Svelte: [.svelte](docs/parsers/svelte.md)  \n- Vue: [.vue](docs/parsers/vue.md)  \n- Astro: [.astro](docs/parsers/astro.md)  \n- Angular: [.html, .ts](docs/parsers/angular.md)  \n- HTML: [.html](docs/parsers/html.md)  \n- CSS: [.css](docs/parsers/css.md)  \n- JavaScript: [.js](docs/parsers/javascript.md)  \n- TypeScript: [.ts](docs/parsers/typescript.md)  \n\n<br/>\n<br/>\n\n### Rules\n\nThe rules are categorized into two types: `stylistic` and `correctness`.\n\n#### Configs\n\nThe plugin offers three recommended configurations to help you get started quickly:\n  \n- `stylistic`: Enforces stylistic rules for tailwind classes.\n- `correctness`: Enforces correctness rules for tailwind classes.\n- `recommended`: Enforces both stylistic and correctness rules.\n  \nBy default:\n\n- `stylistic` rules are reported as warnings\n- `correctness` rules are reported as errors\n  \nYou can change the severity by adding a suffix to the config name:\n  \n- Use `-error` to report all rules as errors\n- Use `-warn` to report all rules as warnings\n  \nFor example, `recommended-warn` will report every rule as a warning and `stylistic-error` will report the formatting rules as errors.\n  \nIf you still use the old .eslintrc configuration format, you can prefix the config names with `legacy-`.\n\nFor example, `legacy-recommended` or `legacy-correctness-warn`.\n\nThe table below lists all available rules, the Tailwind CSS versions they support, and whether they are enabled by default in each recommended configuration:\n\n<br/>\n<br/>\n\n#### Stylistic rules\n\n| Name | Description | `tw3` | `tw4` | `recommended` | autofix |\n| :--- | :--- | :---: | :---: | :---: | :---: |\n| [enforce-consistent-line-wrapping](docs/rules/enforce-consistent-line-wrapping.md) | Enforce consistent line wrapping for tailwind classes. | ✔ | ✔ | ✔ | ✔ |\n| [enforce-consistent-class-order](docs/rules/enforce-consistent-class-order.md) | Enforce a consistent order for tailwind classes. | ✔ | ✔ | ✔ | ✔ |\n| [enforce-consistent-variant-order](docs/rules/enforce-consistent-variant-order.md) | Enforce a consistent variant order for tailwind classes. |  | ✔ |  | ✔ |\n| [enforce-consistent-variable-syntax](docs/rules/enforce-consistent-variable-syntax.md) | Enforce consistent variable syntax. | ✔ | ✔ |  | ✔ |\n| [enforce-consistent-important-position](docs/rules/enforce-consistent-important-position.md) | Enforce consistent position of the important modifier. | ✔ | ✔ |  | ✔ |\n| [enforce-shorthand-classes](docs/rules/enforce-shorthand-classes.md) | Enforce shorthand class names. | ✔ | ✔ |  | ✔ |\n| [enforce-logical-properties](docs/rules/enforce-logical-properties.md) | Enforce logical property class names. | ✔ | ✔ |  | ✔ |\n| [enforce-canonical-classes](docs/rules/enforce-canonical-classes.md) | Enforce canonical class names. |  | ✔ | ✔ | ✔ |\n| [no-duplicate-classes](docs/rules/no-duplicate-classes.md) | Remove duplicate classes. | ✔ | ✔ | ✔ | ✔ |\n| [no-deprecated-classes](docs/rules/no-deprecated-classes.md) | Remove deprecated classes. |  | ✔ | ✔ | ✔ |\n| [no-unnecessary-whitespace](docs/rules/no-unnecessary-whitespace.md) | Disallow unnecessary whitespace in tailwind classes. | ✔ | ✔ | ✔ | ✔ |\n\n#### Correctness rules\n\n| Name | Description | `tw3` | `tw4` | `recommended` | autofix |\n| :--- | :--- | :---: | :---: | :---: | :---: |\n| [no-unknown-classes](docs/rules/no-unknown-classes.md) | Report classes not registered with Tailwind CSS. | ✔ | ✔ | ✔ |  |\n| [no-conflicting-classes](docs/rules/no-conflicting-classes.md) | Report classes that produce conflicting styles. |  | ✔ | ✔ |  |\n| [no-restricted-classes](docs/rules/no-restricted-classes.md) | Disallow restricted classes. | ✔ | ✔ |  | ✔ |\n\n<br/>\n<br/>\n\n### Utilities\n\nThis plugin is pre-configured to lint tailwind classes for the most popular utilities:\n\n- [tailwind merge](https://github.com/dcastil/tailwind-merge): `twMerge` · `twJoin`\n- [class variance authority](https://github.com/joe-bell/cva): `cva`\n- [tailwind variants](https://github.com/nextui-org/tailwind-variants?tab=readme-ov-file): `tv`\n- [shadcn](https://ui.shadcn.com/docs/installation/manual): `cn`\n- [classcat](https://github.com/jorgebucaran/classcat): `cc`\n- [class list builder](https://github.com/crswll/clb): `clb`\n- [clsx](https://github.com/lukeed/clsx): `clsx`\n- [cnbuilder](https://github.com/xobotyi/cnbuilder): `cnb`\n- [classnames template literals](https://github.com/netlify/classnames-template-literals): `ctl`\n- [obj str](https://github.com/lukeed/obj-str): `objstr`\n- [react-twc](https://github.com/gregberge/twc): `twc` · `twx`\n\n<br/>\n<br/>\n\n### Advanced configuration\n\nIf an utility is not supported by default, or you want to customize the configuration, you can define which [attributes](./docs/configuration/advanced.md#attribute), [callees](./docs/configuration/advanced.md#callee), [variables](./docs/configuration/advanced.md#variable), and [tags](./docs/configuration/advanced.md#tag) should get linted.  \nSee the [Advanced configuration guide](./docs/configuration/advanced.md) to learn how to override or extend the default settings.\n\n<br/>\n<br/>\n\n### Monorepo setup\n\nIn monorepos, linting is often started from the repository root while each package has its own Tailwind setup.\nYou can configure `settings[\"better-tailwindcss\"].cwd` per file group so the plugin resolves `tailwindcss` and config files from the correct project directory.\n\n```js\n// eslint.config.js\nexport default [\n  {\n    files: [\"packages/website/**/*.{js,jsx,cjs,mjs,ts,tsx}\"],\n    settings: {\n      \"better-tailwindcss\": {\n        cwd: \"./packages/website\"\n      }\n    }\n  },\n  {\n    files: [\"packages/app/**/*.{js,jsx,cjs,mjs,ts,tsx}\"],\n    settings: {\n      \"better-tailwindcss\": {\n        cwd: \"./packages/app\"\n      }\n    }\n  }\n];\n```\n\nSee [Settings](./docs/settings/settings.md#cwd) for more details.\n\n<br/>\n<br/>\n\n### Editor configuration\n\n#### VSCode\n\n##### Auto-fix on save\n\nMost rules are intended to automatically fix the tailwind classes using VSCode extensions.\n\n###### ESLint\n\nFor ESLint, you can install the [VSCode ESLint plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and configure it to automatically fix the classes on save by adding the following options to your `.vscode/settings.json`:\n\n```jsonc\n{\n  // enable VSCode to fix tailwind classes on save\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  }\n}\n```\n\n###### Oxlint\n\nFor Oxlint, you can install the [VSCode Oxc plugin](https://marketplace.visualstudio.com/items?itemName=oxc.oxc-vscode) and configure it to automatically fix the classes on save by adding the following options to your `.vscode/settings.json`:\n\n```jsonc\n{\n  // enable VSCode to fix tailwind classes on save\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.oxc\": \"explicit\"\n  }\n}\n```\n\n<br/>\n<br/>\n"
  },
  {
    "path": "build/index.ts",
    "content": "import { $ } from \"better-tailwindcss:build/utils.js\";\n\nasync function build(){\n  const outDir = \"lib\"\n\n  console.info(\"Building...\")\n  await $(`npx tsc --project tsconfig.build.json --outDir ${outDir}`)\n  await $(`npx tsc-alias --outDir ${outDir}`)\n  console.info(\"Build complete\")\n}\n\nbuild().catch(console.error);\n"
  },
  {
    "path": "build/utils.ts",
    "content": "import { exec, type ExecOptions } from 'node:child_process'\n\nexport async function $(command: string, options?: ExecOptions): Promise<string | Buffer> {\n    return new Promise((resolve, reject) => {\n        exec(command, options, (error, stdout, stderr) => {\n            if (error) {\n                reject(error || stderr)\n            }\n            resolve(stdout)\n        })\n    })\n}\n"
  },
  {
    "path": "changelog.config.js",
    "content": "export { default } from \"@schoero/configs/changelogen\";\n"
  },
  {
    "path": "docs/api/defaults.md",
    "content": "\n# Defaults\n\nThe plugin comes with a set of default [selectors](../configuration/advanced.md#selectors). These selectors are used to [determine how the rules should behave](../configuration/advanced.md#advanced-configuration) when checking your code.\nIn order to extend the default configuration instead of overwriting it, you can import the default options from `eslint-plugin-better-tailwindcss/defaults` and merge them with your own options.\n\n<br/>\n<br/>\n\n## Extending the config\n\n```ts\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { getDefaultSelectors } from \"eslint-plugin-better-tailwindcss/defaults\";\nimport { MatcherType, SelectorKind } from \"eslint-plugin-better-tailwindcss/types\";\n\n\nexport default [\n  {\n    plugins: {\n      \"better-tailwindcss\": eslintPluginBetterTailwindcss\n    },\n    rules: {\n      \"better-tailwindcss/enforce-consistent-class-order\": [\"warn\", {\n        selectors: [\n          ...getDefaultSelectors(),\n          // custom tag\n          {\n            kind: SelectorKind.Tag,\n            match: [\n              {\n                type: MatcherType.String\n              }\n            ],\n            name: \"^myTag$\"\n          },\n          // custom callee\n          {\n            kind: SelectorKind.Callee,\n            match: [\n              {\n                type: MatcherType.String\n              }\n            ],\n            name: \"^myFunction$\"\n          },\n          // custom attribute\n          {\n            kind: SelectorKind.Attribute,\n            match: [\n              {\n                type: MatcherType.String\n              }\n            ],\n            name: \"^myAttribute$\"\n          },\n          // custom variable\n          {\n            kind: SelectorKind.Variable,\n            match: [\n              {\n                type: MatcherType.String\n              }\n            ],\n            name: \"^myVariable$\"\n          }\n        ]\n      }]\n    }\n  }\n];\n```\n"
  },
  {
    "path": "docs/configuration/advanced.md",
    "content": "# Advanced Configuration\n\nThe [rules](../../README.md#rules) in this plugin lint Tailwind classes inside string literals.\n\nTo do that safely, the plugin must know **which strings are expected to contain Tailwind classes**. If it would lint every string literal in your codebase, it would produce many false positives and potentially unsafe fixes.\n\nTo configure this, you can provide an array of [selectors](#selectors) that specify where the plugin should look for class strings and how to extract them.\n\nThe plugin already ships with defaults that support [most popular tailwind utilities](../../README.md#utilities). You only need advanced configuration when:\n\n- you use custom utilities/APIs not covered by defaults,\n- you want to narrow down linting behavior,\n- or you want to lint additional locations.\n\nTo extend defaults instead of replacing them, import and spread `getDefaultSelectors()` from `eslint-plugin-better-tailwindcss/defaults`.\n\nYou can find the default selectors in the [defaults documentation](../api/defaults.md).\n\n<br/>\n<br/>\n\n## Selectors\n\nEach selector targets one kind of source location and tells the plugin how to extract class strings from it.\n\nThe plugin supports four selector types: `attribute`, `callee`, `variable`, and `tag`.\nEvery selector can then match different types of string literals based on the provided `match` option.\n\n### Type\n\n<br/>\n\n### `attribute`\n\n- **kind**: `\"attribute\"`.  \n- **name**: regular expression for attribute names.  \n- **match** `optional`: [selector matcher](#selector-matcher-types) list.  \n  When omitted, only direct string literals are collected.  \n\n```ts\ntype AttributeSelector = {\n  kind: \"attribute\";\n  name: string;\n  match?: SelectorMatcher[];\n};\n```\n\n<br/>\n\n### `callee`\n\n- **kind**: `\"callee\"`.  \n- **name** `optional`: regular expression for callee names.  \n- **path** `optional`: regular expression for callee member paths like `classes.push`.  \n  When `path` is provided, `name` is not required.  \n- **targetCall** `optional`: curried call target for example for `fn()(\"my classes\")`.  \n  If a non-negative number is provided, the zero-based call index is used.  \n  Negative numbers count from the end (`-1` is the last call).  \n  When omitted, the first call in a curried chain is used.  \n- **targetArgument** `optional`: target specific call arguments.  \n  If a non-negative number is provided, the zero-based argument index is used.  \n  Negative numbers count from the end (`-1` is the last argument).  \n  When omitted, all arguments of the selected call are checked.  \n- **match** `optional`: [selector matcher](#selector-matcher-types) list.  \n  When omitted, only direct string literals are collected.  \n\n```ts\ntype CalleeSelector = {\n  kind: \"callee\";\n  match?: SelectorMatcher[];\n  name?: string;\n  path?: string;\n  targetArgument?: \"all\" | \"first\" | \"last\" | number;\n  targetCall?: \"all\" | \"first\" | \"last\" | number;\n};\n```\n\n<br/>\n\n### `variable`\n\n- **kind**: `\"variable\"`.  \n- **name**: regular expression for variable names.  \n  Tip: The name `default` targets the `export default ...` declaration.  \n- **match** `optional`: [selector matcher](#selector-matcher-types) list.  \n  When omitted, only direct string literals are collected.  \n\n```ts\ntype VariableSelector = {\n  kind: \"variable\";\n  name: string;\n  match?: SelectorMatcher[];\n};\n```\n\n<br/>\n\n### `tag`\n\n- **kind**: `\"tag\"`.  \n- **name**: `optional` regular expression for tagged template names.  \n- **path** `optional`: regular expression for tagged template member paths like `twc.class`.  \n  When `path` is provided, `name` is not required.  \n- **match** `optional`: [selector matcher](#selector-matcher-types) list.  \n  When omitted, only direct string literals are collected.  \n\n```ts\ntype TagSelector = {\n  kind: \"tag\";\n  name: string;\n  match?: SelectorMatcher[];\n};\n```\n\n<br/>\n\n### How selector matching works\n\n- Names are treated as regular expressions.\n- Reserved regex characters must be escaped.\n- The regex must match the whole name (not a substring).\n\n```jsonc\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"path\": \"^classes\\\\.push$\",\n      \"match\": [{ \"type\": \"strings\" }]\n    }\n  ]\n}\n```\n\n<br/>\n<br/>\n\n### Matchers\n\n#### Selector matcher types\n\n##### `strings`\n\nMatches all string literals that are not object keys or object values.\n\n```ts\ntype SelectorStringMatcher = {\n  type: \"strings\";\n};\n```\n\n```json\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^tw$\",\n      \"match\": [\n        { \"type\": \"strings\" }\n      ]\n    }\n  ]\n}\n```\n\nMatches:\n\n```tsx\ntw(\n  \"this will get linted\",\n  { className: \"this will not get linted by this matcher\" }\n);\n```\n\n<br />\n\n##### `objectKeys`\n\nMatches all object keys.\n\n- `path` `optional`: regular expression to narrow matching to specific object key paths\n  See [Path option details](#path-option-details).\n\n```ts\ntype SelectorObjectKeyMatcher = {\n  type: \"objectKeys\";\n  path?: string;\n};\n```\n\n```json\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^tw$\",\n      \"match\": [\n        {\n          \"type\": \"objectKeys\",\n          \"path\": \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nMatches:\n\n```tsx\ntw({\n  compoundVariants: [\n    {\n      className: \"<- this key will get linted\",\n      myVariant: \"but this key will not get linted\"\n    }\n  ]\n});\n```\n\n<br />\n\n##### `objectValues`\n\nMatches all object values.\n\n- `path` `optional`: regular expression to narrow matching to specific object value paths\n  See [Path option details](#path-option-details).\n  \n```ts\ntype SelectorObjectValueMatcher = {\n  type: \"objectValues\";\n  path?: string;\n};\n```\n\n```json\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^tw$\",\n      \"match\": [\n        {\n          \"type\": \"objectValues\",\n          \"path\": \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nMatches:\n\n```tsx\ntw({\n  compoundVariants: [\n    {\n      className: \"this value will get linted\",\n      myVariant: \"but this value will not get linted\"\n    }\n  ]\n});\n```\n\n<br />\n\n##### `anonymousFunctionReturn`\n\nMatches values returned from anonymous functions and applies nested matchers to those return values.\n\n- `match` `required`: nested matcher array  \n  The nested `match` array can include `strings`, `objectKeys`, and `objectValues` matchers.  \n\n```ts\ntype SelectorAnonymousFunctionReturnMatcher = {\n  match: (SelectorObjectKeyMatcher | SelectorObjectValueMatcher | SelectorStringMatcher)[];\n  type: \"anonymousFunctionReturn\";\n};\n```\n\n```json\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^tw$\",\n      \"match\": [\n        {\n          \"type\": \"anonymousFunctionReturn\",\n          \"match\": [\n            { \"type\": \"strings\" },\n            { \"type\": \"objectKeys\" },\n            { \"type\": \"objectValues\" }\n          ]\n        }\n      ]\n    }\n  ]\n}\n```\n\nMatches:\n\n```tsx\ntw(() => \"this will get linted with a nested string matcher\");\ntw(() => ({ className: \"<- this key will get linted with a nested objectKeys matcher\" }));\ntw(() => ({ className: \"this will get linted with nested objectValues matcher\" }));\n```\n\n<br/>\n\n##### Path option details\n\nThe `path` option lets you narrow down `objectKeys` and `objectValues` matching to specific object paths.  \n  \nThis is especially useful for libraries like [Class Variance Authority (cva)](https://cva.style/docs/getting-started/installation#intellisense), where class names appear in nested object structures.  \n  \n`path` is a regex matched against the object path.  \n  \nFor example, the following matcher will only match object values for the `compoundVariants.class` key:\n\n<br/>\n\n```json\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^cva$\",\n      \"match\": [\n        {\n          \"type\": \"objectValues\",\n          \"path\": \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n```tsx\n<img class={\n  cva(\"this will not get linted\", {\n    compoundVariants: [\n      {\n        class: \"but this will get linted\",\n        myVariant: \"and this will not get linted\"\n      }\n    ]\n  })\n} />;\n```\n\n<br/>\n\nThe path reflects how the string is nested in the object:  \n  \n- Dot notation for plain keys: `root.nested.values`  \n- Square brackets for arrays: `values[0]`  \n- Quoted brackets for special characters: `root[\"some-key\"]`  \n  \nFor example, the object path for `value` in the object below is `root[\"nested-key\"].values[0].value`:\n\n```json\n{\n  \"root\": {\n    \"nested-key\": {\n      \"values\": [\n        {\n          \"value\": \"this will get linted\"\n        }\n      ]\n    }\n  }\n}\n```\n\n<br/>\n\n### Examples\n\n#### Example: lint only the first argument of the last curried call\n\n```jsonc\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^tw$\",\n      \"targetCall\": \"last\",\n      \"targetArgument\": \"first\"\n    }\n  ]\n}\n```\n\n```tsx\ntw(\"keep\", \"ignore\")(\"this will get linted\", \"this will not\");\n```\n\n#### Example: lint `cva` strings + specific nested values\n\n```jsonc\n{\n  \"selectors\": [\n    {\n      \"kind\": \"callee\",\n      \"name\": \"^cva$\",\n      \"match\": [\n        {\n          \"type\": \"strings\"\n        },\n        {\n          \"type\": \"objectValues\",\n          \"path\": \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n```tsx\n<img class={\n  cva(\"this will get linted\", {\n    compoundVariants: [\n      {\n        class: \"and this will get linted\",\n        myVariant: \"but this will not get linted\"\n      }\n    ]\n  })\n} />;\n```\n\n#### Full example: custom Algolia attribute selector\n\nYou can match custom attributes by modifying your `selectors` configuration. Here is an example on how to match the values inside the Algolia `classNames` objects:\n\n```tsx\n<SearchBox\n  classNames={{\n    form: \"relative\",\n    root: \"p-3 shadow-sm\"\n  }}\n/>;\n```\n\n<br/>\n\n```js\n// eslint.config.js\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { getDefaultSelectors } from \"eslint-plugin-better-tailwindcss/defaults\";\nimport { SelectorKind } from \"eslint-plugin-better-tailwindcss/types\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig({\n  plugins: {\n    \"better-tailwindcss\": eslintPluginBetterTailwindcss\n  },\n  settings: {\n    \"better-tailwindcss\": {\n      entryPoint: \"app/globals.css\",\n      selectors: [\n        ...getDefaultSelectors(), // preserve default selectors\n        {\n          kind: SelectorKind.Attribute,\n          match: [{ type: \"objectValues\" }],\n          name: \"^classNames$\"\n        }\n      ]\n    }\n  }\n});\n// ...\n```\n"
  },
  {
    "path": "docs/parsers/angular.md",
    "content": "# Angular\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with Angular, install [Angular ESLint](https://github.com/angular-eslint/angular-eslint?tab=readme-ov-file#quick-start) and [TypeScript ESLint](https://typescript-eslint.io/getting-started). You can follow the [flat config](https://github.com/angular-eslint/angular-eslint/blob/main/docs/CONFIGURING_FLAT_CONFIG.md) setup, which includes rules from the Angular ESLint package or you can add the parser directly by following the steps below.\n\n```sh\nnpm i -D angular-eslint typescript-eslint\n```\n\nTo lint Tailwind CSS classes in Angular files, ensure that:\n\n- The `angular-eslint` package is installed and configured.\n- The `typescript-eslint` package is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintParserAngular from \"angular-eslint\";\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport { parser as eslintParserTypeScript } from \"typescript-eslint\";\n\nexport default defineConfig([\n  {\n    // enable all recommended rules\n    extends: [\n      eslintPluginBetterTailwindcss.configs.recommended\n    ],\n\n    // if needed, override rules to configure them individually\n    // rules: {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n    // },\n\n    settings: {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        entryPoint: \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        tailwindConfig: \"tailwind.config.js\"\n      }\n    }\n  },\n\n  {\n\n    files: [\"**/*.ts\"],\n    languageOptions: {\n      parser: eslintParserTypeScript,\n      parserOptions: {\n        project: true\n      }\n    },\n    processor: eslintParserAngular.processInlineTemplates\n  },\n\n  {\n    files: [\"**/*.html\"],\n    languageOptions: {\n      parser: eslintParserAngular.templateParser\n    }\n  }\n]);\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n  \n  To use ESLint with Angular using the legacy config, install [Angular ESLint](https://github.com/angular-eslint/angular-eslint?tab=readme-ov-file#quick-start) and [@typescript-eslint/parser](https://typescript-eslint.io/getting-started/legacy-eslint-setup). You can follow the [legacy config](https://github.com/angular-eslint/angular-eslint/blob/main/docs/CONFIGURING_ESLINTRC.md) setup, which includes rules from the Angular ESLint package or you can add the parser directly by following the steps below.\n\n  ```sh\n  npm i -D angular-eslint @typescript-eslint/parser\n  ```\n\n  To lint Tailwind CSS classes in TypeScript files, ensure that:\n\n- The `angular-eslint` package is installed and configured.\n- The `@typescript-eslint/parser` is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"overrides\": [\n      {\n        \"files\": [\"**/*.ts\"],\n        \"parser\": \"@typescript-eslint/parser\",\n        \"extends\": [\n          \"plugin:@angular-eslint/template/process-inline-templates\"\n        ]\n      },\n      {\n        \"files\": [\"**/*.html\"],\n        \"parser\": \"@angular-eslint/template-parser\"\n      }\n    ]\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nOxlint currently does **not** support Angular templates and inline template processing.\nFramework-specific parsers/processors like Angular are not supported in Oxlint yet, so `eslint-plugin-better-tailwindcss` cannot currently lint Angular templates through Oxlint.\n\nYou can continue using ESLint for Angular files until Oxlint adds framework parser support.\n"
  },
  {
    "path": "docs/parsers/astro.md",
    "content": "# Astro\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with Astro files, first install the [astro-eslint-parser](https://github.com/ota-meshi/astro-eslint-parser) and optionally [TypeScript ESLint](https://typescript-eslint.io/getting-started). Then, configure ESLint to use this parser for Astro files.\n\n```sh\nnpm i -D astro-eslint-parser typescript-eslint\n```\n\nTo lint Tailwind CSS classes in Astro files, ensure that:\n\n- The `astro-eslint-parser` is installed and configured.\n- The `typescript-eslint` package is installed if you want to lint TypeScript within Astro files.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintParserAstro from \"astro-eslint-parser\";\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport { parser as eslintParserTypeScript } from \"typescript-eslint\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  files: [\"**/*.astro\"],\n\n  languageOptions: {\n    parser: eslintParserAstro,\n    parserOptions: {\n      // optionally use TypeScript parser within for Astro files\n      parser: eslintParserTypeScript\n    }\n  }\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  To use ESLint with Astro files using the legacy config, first install the [astro-eslint-parser](https://github.com/ota-meshi/astro-eslint-parser) and optionally [@typescript-eslint/parser](https://typescript-eslint.io/getting-started/legacy-eslint-setup). Then, configure ESLint to use this parser for Astro files.\n\n  ```sh\n  npm i -D astro-eslint-parser @typescript-eslint/parser\n  ```\n\n  To lint Tailwind CSS classes in TypeScript files, ensure that:\n\n- The `astro-eslint-parser` is installed and configured.\n- The `@typescript-eslint/parser` is installed and configured if you want to lint TypeScript within Astro files.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parser\": \"astro-eslint-parser\",\n\n    \"parserOptions\": {\n      // optionally use TypeScript parser within for Astro files\n      \"parser\": \"@typescript-eslint/parser\"\n    }\n\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nOxlint currently does **not** support Astro files (`.astro`).\nFramework-specific parsers like Astro are not supported in Oxlint yet, so `eslint-plugin-better-tailwindcss` cannot currently lint Astro templates through Oxlint.\n\nYou can continue using ESLint for Astro files until Oxlint adds framework parser support.\n"
  },
  {
    "path": "docs/parsers/css.md",
    "content": "# CSS\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with CSS files containing Tailwind CSS `@apply` directives, first install the [@eslint/css](https://github.com/eslint/css) plugin and the [tailwind-csstree](https://www.npmjs.com/package/tailwind-csstree) custom syntax.\n\n```sh\nnpm i -D @eslint/css tailwind-csstree\n```\n\nTo lint Tailwind CSS classes in CSS files, ensure that:\n\n- The `@eslint/css` plugin is installed and configured.\n- The `tailwind-csstree` custom syntax is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport css from \"@eslint/css\";\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport { tailwind4 } from \"tailwind-csstree\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  files: [\"**/*.css\"],\n\n  language: \"css/css\",\n\n  languageOptions: {\n    customSyntax: tailwind4,\n    tolerant: true\n  },\n\n  plugins: {\n    css\n  }\n});\n```\n\n<br/>\n\n> **Note:** Legacy config is not supported for CSS files as the `@eslint/css` plugin requires the ESLint flat config format.\n\n<br/>\n\n## Oxlint\n\nOxlint currently does **not** support CSS parser integration for this use case.\nBecause Oxlint currently only supports JavaScript-like files, `eslint-plugin-better-tailwindcss` cannot currently lint CSS `@apply` directives through Oxlint.\n\nYou can continue using ESLint for CSS files until broader parser support is available in Oxlint.\n"
  },
  {
    "path": "docs/parsers/html.md",
    "content": "# HTML\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with HTML files, first install the [@html-eslint/parser](https://github.com/yeonjuan/html-eslint/tree/main/packages/parser).\n\n```sh\nnpm i -D @html-eslint/parser\n```\n\nTo lint Tailwind CSS classes in HTML files, ensure that:\n\n- The `@html-eslint/parser` is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintParserHTML from \"@html-eslint/parser\";\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  files: [\"**/*.html\"],\n\n  languageOptions: {\n    parser: eslintParserHTML\n  }\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parser\": \"@html-eslint/parser\"\n\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nOxlint currently does **not** support HTML parser integration for this use case.\nBecause Oxlint currently only supports JavaScript-like files, `eslint-plugin-better-tailwindcss` cannot currently lint standalone HTML files through Oxlint.\n\nYou can continue using ESLint for HTML files until broader parser support is available in Oxlint.\n"
  },
  {
    "path": "docs/parsers/javascript.md",
    "content": "# JavaScript\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo lint Tailwind CSS classes in JavaScript files, ensure that:\n\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  }\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    }\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nMore info about the Oxlint configuration format can be found in the [Oxlint documentation](https://oxc.rs/docs/guide/usage/linter/config.html).\n\nTo lint Tailwind CSS classes in JavaScript files, ensure that:\n\n- The plugin is added to the `jsPlugins` array.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n- All relevant rules are added to the `rules` object.\n\n<br/>\n\n```js\n// oxlint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"oxlint\";\n\nexport default defineConfig({\n  overrides: [{\n    files: [\"**/*.{js,cjs,mjs}\"],\n    jsPlugins: [\n      \"eslint-plugin-better-tailwindcss\"\n    ],\n    rules: {\n      // enable all recommended rules\n      ...eslintPluginBetterTailwindcss.configs.recommended.rules,\n\n      // if needed, override rules to configure them individually\n      \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n    }\n  }],\n  settings: {\n    \"better-tailwindcss\": {\n      entryPoint: \"src/global.css\"\n    }\n  }\n});\n```\n"
  },
  {
    "path": "docs/parsers/jsx.md",
    "content": "# JSX\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo lint Tailwind CSS classes in JSX files, ensure that:\n\n- `jsx` parsing is enabled in language options.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  languageOptions: {\n    parserOptions: {\n      ecmaFeatures: {\n        jsx: true\n      }\n    }\n  }\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parserOptions\": {\n      \"ecmaFeatures\": {\n        \"jsx\": true\n      },\n      \"ecmaVersion\": \"latest\"\n    }\n\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nMore info about the Oxlint configuration format can be found in the [Oxlint documentation](https://oxc.rs/docs/guide/usage/linter/config.html).\n\nTo lint Tailwind CSS classes in JSX files, ensure that:\n\n- The plugin is added to the `jsPlugins` array.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n- All relevant rules are added to the `rules` object.\n\n<br/>\n\n```js\n// oxlint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"oxlint\";\n\nexport default defineConfig({\n  overrides: [{\n    files: [\"**/*.{js,jsx,mjs,cjs}\"],\n    jsPlugins: [\n      \"eslint-plugin-better-tailwindcss\"\n    ],\n    rules: {\n      // enable all recommended rules\n      ...eslintPluginBetterTailwindcss.configs.recommended.rules,\n\n      // if needed, override rules to configure them individually\n      \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n    }\n  }],\n  settings: {\n    \"better-tailwindcss\": {\n      entryPoint: \"src/global.css\"\n    }\n  }\n});\n```\n"
  },
  {
    "path": "docs/parsers/svelte.md",
    "content": "# Svelte\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with Svelte files, first install the [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser).\n\n```sh\nnpm i -D svelte-eslint-parser\n```\n\nTo lint Tailwind CSS classes in Svelte files, ensure that:\n\n- The `svelte-eslint-parser` is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport eslintParserSvelte from \"svelte-eslint-parser\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  files: [\"**/*.svelte\"],\n\n  languageOptions: {\n    parser: eslintParserSvelte\n  }\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parser\": \"svelte-eslint-parser\"\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nOxlint currently does **not** support Svelte files (`.svelte`).\nFramework-specific parsers like Svelte are not supported in Oxlint yet, so `eslint-plugin-better-tailwindcss` cannot currently lint Svelte templates through Oxlint.\n\nYou can continue using ESLint for Svelte files until Oxlint adds framework parser support.\n"
  },
  {
    "path": "docs/parsers/tsx.md",
    "content": "# TSX\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with TSX files, first install the [typescript-eslint](https://typescript-eslint.io/getting-started) package.\n\n```sh\nnpm i -D typescript-eslint\n```\n\nTo lint Tailwind CSS classes in TSX files, ensure that:\n\n- The `typescript-eslint` package is installed and configured.\n- `jsx` parsing is enabled in language options.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport { parser as eslintParserTypeScript } from \"typescript-eslint\";\n\nexport default defineConfig([\n  {\n    // enable all recommended rules\n    extends: [\n      eslintPluginBetterTailwindcss.configs.recommended\n    ],\n\n    // if needed, override rules to configure them individually\n    // rules: {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n    // },\n\n    settings: {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        entryPoint: \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        tailwindConfig: \"tailwind.config.js\"\n      }\n    }\n  },\n\n  {\n    files: [\"**/*.{ts,tsx,cts,mts}\"],\n    languageOptions: {\n      parser: eslintParserTypeScript,\n      parserOptions: {\n        project: true\n      }\n    }\n  },\n\n  {\n    files: [\"**/*.{jsx,tsx}\"],\n    languageOptions: {\n      parserOptions: {\n        ecmaFeatures: {\n          jsx: true\n        }\n      }\n    }\n  }\n]);\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  To use ESLint with TypeScript files using the legacy config, first install the [@typescript-eslint/parser](https://typescript-eslint.io/getting-started/legacy-eslint-setup).\n\n  ```sh\n  npm i -D @typescript-eslint/parser\n  ```\n\n  To lint Tailwind CSS classes in TypeScript files, ensure that:\n\n- The `@typescript-eslint/parser` is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n    \n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parser\": \"@typescript-eslint/parser\",\n\n    \"parserOptions\": {\n      \"ecmaFeatures\": {\n        \"jsx\": true\n      },\n      \"ecmaVersion\": \"latest\"\n    }\n\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nMore info about the Oxlint configuration format can be found in the [Oxlint documentation](https://oxc.rs/docs/guide/usage/linter/config.html).\n\nTo lint Tailwind CSS classes in TSX files, ensure that:\n\n- The plugin is added to the `jsPlugins` array.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n- All relevant rules are added to the `rules` object.\n\n<br/>\n\n```ts\n// oxlint.config.ts\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"oxlint\";\n\nexport default defineConfig({\n  overrides: [{\n    files: [\"**/*.{js,cjs,mjs,ts,tsx,cts,mts}\"],\n    jsPlugins: [\n      \"eslint-plugin-better-tailwindcss\"\n    ],\n    rules: {\n      // enable all recommended rules\n      ...eslintPluginBetterTailwindcss.configs.recommended.rules,\n\n      // if needed, override rules to configure them individually\n      \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n    }\n  }],\n  settings: {\n    \"better-tailwindcss\": {\n      entryPoint: \"src/global.css\"\n    }\n  }\n});\n```\n"
  },
  {
    "path": "docs/parsers/typescript.md",
    "content": "# TypeScript\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with TypeScript files, first install the [typescript-eslint](https://typescript-eslint.io/getting-started) package.\n\n```sh\nnpm i -D typescript-eslint\n```\n\nTo lint Tailwind CSS classes in TypeScript files, ensure that:\n\n- The `typescript-eslint` package is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport { parser as eslintParserTypeScript } from \"typescript-eslint\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  files: [\"**/*.{ts,tsx,cts,mts}\"],\n\n  languageOptions: {\n    parser: eslintParserTypeScript,\n    parserOptions: {\n      project: true\n    }\n  }\n\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  To use ESLint with TypeScript files using the legacy config, first install the [@typescript-eslint/parser](https://typescript-eslint.io/getting-started/legacy-eslint-setup).\n\n  ```sh\n  npm i -D @typescript-eslint/parser\n  ```\n\n  To lint Tailwind CSS classes in TypeScript files, ensure that:\n\n- The `@typescript-eslint/parser` is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parser\": \"@typescript-eslint/parser\"\n\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nMore info about the Oxlint configuration format can be found in the [Oxlint documentation](https://oxc.rs/docs/guide/usage/linter/config.html).\n\nTo lint Tailwind CSS classes in TypeScript files, ensure that:\n\n- The plugin is added to the `jsPlugins` array.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n- All relevant rules are added to the `rules` object.\n\n<br/>\n\n```ts\n// oxlint.config.ts\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"oxlint\";\n\nexport default defineConfig({\n  overrides: [{\n    files: [\"**/*.{js,cjs,mjs,ts,tsx,cts,mts}\"],\n    jsPlugins: [\n      \"eslint-plugin-better-tailwindcss\"\n    ],\n    rules: {\n      // enable all recommended rules\n      ...eslintPluginBetterTailwindcss.configs.recommended.rules,\n\n      // if needed, override rules to configure them individually\n      \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n    }\n  }],\n  settings: {\n    \"better-tailwindcss\": {\n      entryPoint: \"src/global.css\"\n    }\n  }\n});\n```\n"
  },
  {
    "path": "docs/parsers/vue.md",
    "content": "# Vue\n\n- [ESLint](#eslint)\n- [Oxlint](#oxlint)\n\n<br/>\n\n## ESLint\n\nTo use ESLint with Vue files, first install the [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser).\n\n```sh\nnpm i -D vue-eslint-parser\n```\n\nTo lint Tailwind CSS classes in Vue files, ensure that:\n\n- The `vue-eslint-parser` is installed and configured.\n- The plugin is added to your configuration.\n- The `settings` object contains the correct Tailwind CSS configuration paths.\n\n<br/>\n\n### Flat config\n\nRead more about the [ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new)\n\n<br/>\n\n```js\n// eslint.config.js\n\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\nimport { defineConfig } from \"eslint/config\";\nimport eslintParserVue from \"vue-eslint-parser\";\n\nexport default defineConfig({\n  // enable all recommended rules\n  extends: [\n    eslintPluginBetterTailwindcss.configs.recommended\n  ],\n\n  // if needed, override rules to configure them individually\n  // rules: {\n  //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { printWidth: 100 }]\n  // },\n\n  settings: {\n    \"better-tailwindcss\": {\n      // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n      entryPoint: \"src/global.css\",\n      // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n      tailwindConfig: \"tailwind.config.js\"\n    }\n  },\n\n  files: [\"**/*.vue\"],\n\n  languageOptions: {\n    parser: eslintParserVue\n  }\n});\n```\n\n<br/>\n\n<details>\n  <summary><h3>Legacy config</h3></summary>\n\n  <br/>\n\n  ```jsonc\n  // .eslintrc.json\n\n  {\n    // enable all recommended rules\n    \"extends\": [\n      \"plugin:better-tailwindcss/legacy-recommended\"\n    ],\n\n    // if needed, override rules to configure them individually\n    // \"rules\": {\n    //   \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", { \"printWidth\": 100 }]\n    // },\n\n    \"settings\": {\n      \"better-tailwindcss\": {\n        // tailwindcss 4: the path to the entry file of the css based tailwind config (eg: `src/global.css`)\n        \"entryPoint\": \"src/global.css\",\n        // tailwindcss 3: the path to the tailwind config file (eg: `tailwind.config.js`)\n        \"tailwindConfig\": \"tailwind.config.js\"\n      }\n    },\n\n    \"parser\": \"vue-eslint-parser\"\n  }\n  ```\n\n</details>\n\n<br/>\n\n## Oxlint\n\nOxlint currently does **not** support Vue files (`.vue`).\nFramework-specific parsers like Vue are not supported in Oxlint yet, so `eslint-plugin-better-tailwindcss` cannot currently lint Vue templates through Oxlint.\n\nYou can continue using ESLint for Vue files until Oxlint adds framework parser support.\n"
  },
  {
    "path": "docs/rules/enforce-canonical-classes.md",
    "content": "# better-tailwindcss/enforce-canonical-classes\n\nImplements the [canonical suggestions](https://github.com/tailwindlabs/tailwindcss/pull/19059) from Tailwind CSS `^4.1.15`.\nA canonical class is a simpler representation of a less optimal way of writing the same class. This can be the case when arbitrary values or variants are used while a predefined value exists for example.\n\n> [!NOTE]\n>\n> - This rule is identical to `suggestCanonicalClasses` from the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) VSCode extension.  \n>   It is recommended to disable `suggestCanonicalClasses` in your projects `.vscode/settings.json` to avoid confusion:\n>\n>   ```jsonc\n>   {\n>     \"tailwindCSS.lint.suggestCanonicalClasses\": \"ignore\"\n>   }\n>   ```\n>\n> - The rule covers multiple other rules.  \n>   It is recommended to disable the following rules to avoid duplicate reports:\n>   - [`better-tailwindcss/enforce-shorthand-classes`](./enforce-shorthand-classes.md)\n>   - [`better-tailwindcss/enforce-consistent-important-position`](./enforce-consistent-important-position.md)\n>   - [`better-tailwindcss/enforce-consistent-variable-syntax`](./enforce-consistent-variable-syntax.md)\n>\n> - The canonical suggestions are based on the internal logic of Tailwind CSS and it is possible that the suggestions can change in future versions of Tailwind CSS.\n> - Configurability is also limited to what Tailwind CSS exposes via their API.\n> - The rule comes with a [startup cost of around ~1s](https://github.com/tailwindlabs/tailwindcss/pull/19059#:~:text=performance).\n\n<br/>\n\n## Options\n\n<br/>\n\n### `rootFontSize`\n\nThe font size of the `<html>` element in pixels. By default, the root font size is `16px` unless it is changed with CSS.\nIf provided, this will be used to determine if arbitrary values can be replaced with predefined sizing scales. This can also be configured via the [`settings` object](../settings/settings.md).\n\n**Type**: `number | undefined`  \n**Default**: `undefined`\n\n<br/>\n\n### `collapse`\n\nWhether to collapse multiple utilities into a single utility if possible.  \nIf set to `true`, it is recommended to disable the [`better-tailwindcss/enforce-shorthand-classes`](./enforce-shorthand-classes.md) rule to avoid duplicate reports.\n\n**Type**: `boolean`  \n**Default**: `true`\n\n<br/>\n\n### `logical`\n\nWhether to convert between logical and physical properties when collapsing utilities.\n\n**Type**: `boolean`  \n**Default**: `true`\n\n<br/>\n\n### `ignore`\n\nList of List of regex patterns for classes that should not report a canonical suggestion.\n\nThis can be useful for cases where a non-canonical class representation is intentional.\n\n**Type**: `string[]`  \n**Default**: `[]`\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: using unnecessary arbitrary value\n<div class=\"[display:flex]\" />;\n```\n\n```tsx\n// ✅ GOOD: using canonical class for display flex\n<div class=\"flex\" />;\n```\n\n```tsx\n// ❌ BAD: using unnecessary arbitrary data attribute variant\n<div class=\"data-[is-selected]:opacity-100\" />;\n```\n\n```tsx\n// ✅ GOOD: using canonical data attribute variant\n<div class=\"data-is-selected:opacity-100\" />;\n```\n\n```tsx\n// ❌ BAD: using arbitrary value for spacing\n<div class=\"mt-[16px]\" />;\n```\n\n```tsx\n// ✅ GOOD: using canonical class for spacing (with rootFontSize: 16)\n<div class=\"mt-4\" />;\n```\n\n```tsx\n// ❌ BAD: using multiple utilities for margin\n<div class=\"mt-2 mr-2 mb-2 ml-2\" />;\n```\n\n```tsx\n// ✅ GOOD: using collapsed utility for margin (with collapse: true)\n<div class=\"m-2\" />;\n```\n\n```tsx\n// ❌ BAD: using physical properties for margin\n<div class=\"mr-2 ml-2\" />;\n```\n\n```tsx\n// ✅ GOOD: using logical properties for margin (with logicalToPhysical: true)\n<div class=\"mx-2\" />;\n```\n"
  },
  {
    "path": "docs/rules/enforce-consistent-class-order.md",
    "content": "# better-tailwindcss/enforce-consistent-class-order\n\nEnforce the order of tailwind classes. It is possible to sort classes alphabetically or logically.\n\n<br/>\n\n## Options\n\n### `order`\n\n- `asc`: Sort classes alphabetically in ascending order.\n- `desc`: Sort classes alphabetically in descending order.\n- `official`: Sort classes according to the official sorting order from Tailwind CSS based on semantics.\n- `strict`: Same as `official` but sorts variants more strictly:\n  - Classes that share the same base variants get grouped together.\n  - Classes with less variants come before classes with more variants.\n  - Classes with arbitrary variants come last.\n\n  **Type**: `\"asc\" | \"desc\" | \"official\" | \"strict\"`  \n  **Default**: `\"official\"`\n\n<br/>\n\n### `detectComponentClasses`\n\n  Tailwind CSS v4 allows you to define custom [component classes](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes) like `card`, `btn`, `badge` etc.\n  \n  If you want to create such classes, you can set this option to `true` to allow the rule to detect those classes and not report them as unknown classes. This can also be configured via the [`settings` object](../settings/settings.md).\n\n  **Type**: `boolean`  \n  **Default**: `false`\n\n<br/>\n\n### `componentClassOrder`\n\n  Defines how component classes should be ordered among themselves.\n\n- `asc`: Sort component classes alphabetically in ascending order.\n- `desc`: Sort component classes alphabetically in descending order.\n- `preserve`: Keep component classes in their original order.\n\n  **Type**: `\"asc\" | \"desc\" | \"preserve\"`  \n  **Default**: `\"preserve\"`\n\n<br/>\n\n### `componentClassPosition`\n\n  Defines where component classes should be placed in relation to the whole string literal.\n\n- `start`: Place component classes at the beginning.\n- `end`: Place component classes at the end.\n\n  **Type**: `\"start\" | \"end\"`  \n  **Default**: `\"start\"`\n\n<br/>\n\n### `unknownClassOrder`\n\n  Defines how unknown classes should be ordered among themselves.\n\n- `asc`: Sort unknown classes alphabetically in ascending order.\n- `desc`: Sort unknown classes alphabetically in descending order.\n- `preserve`: Keep unknown classes in their original order.\n\n  **Type**: `\"asc\" | \"desc\" | \"preserve\"`  \n  **Default**: `\"preserve\"`\n\n<br/>\n\n### `unknownClassPosition`\n\n  Defines where unknown classes should be placed in relation to the whole string literal.\n\n- `start`: Place unknown classes at the beginning.\n- `end`: Place unknown classes at the end.\n\n  **Type**: `\"start\" | \"end\"`  \n  **Default**: `\"start\"`\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).  \n  If not specified, the plugin will fall back to the default configuration.  \n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  The path to the `tailwind.config.js` file. If not specified, the plugin will try to find it automatically or falls back to the default configuration.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tailwindConfig).  \n\n  For Tailwind CSS v4 and the css based config, use the [`entryPoint`](#entrypoint) option instead.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tsconfig).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: all classes are in random order\n<div class=\"underline hover:text-opacity-70 focus:font-bold text-black hover:font-bold focus:text-opacity-70\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { order: 'asc' }\n<div class=\"focus:font-bold focus:text-opacity-70 hover:font-bold hover:text-opacity-70 text-black underline\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { order: 'desc' }\n<div class=\"underline text-black hover:text-opacity-70 hover:font-bold focus:text-opacity-70 focus:font-bold\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { order: 'official' }\n<div class=\"text-black underline hover:font-bold hover:text-opacity-70 focus:font-bold focus:text-opacity-70\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { componentClassPosition: 'start' }\n// 'btn' and 'card' are defined as component classes in the tailwind config\n<div class=\"btn card text-black underline\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { componentClassPosition: 'end' }\n// 'btn' and 'card' are defined as component classes in the tailwind config\n<div class=\"text-black underline btn card\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { unknownClassPosition: 'start' }\n// 'unknown-class' is not defined in the tailwind config\n<div class=\"unknown-class text-black underline\"/>;\n```\n\n```tsx\n// ✅ GOOD: with option { unknownClassPosition: 'end' }\n// 'unknown-class' is not defined in the tailwind config\n<div class=\"text-black underline unknown-class\"/>;\n```\n"
  },
  {
    "path": "docs/rules/enforce-consistent-important-position.md",
    "content": "# better-tailwindcss/enforce-consistent-important-position\n\nEnforce consistent important position for Tailwind CSS classes. This rule ensures that the important modifier (`!`) is placed consistently either at the beginning (legacy style) or at the end (recommended style) of class names.\n\nTailwind CSS v4 introduces the \"recommended\" position as the new standard. This rule helps you migrate to the new syntax or maintain consistency with the legacy format.\n\n<br/>\n\n> [!NOTE]\n> This rule might interfere with [`better-tailwindcss/enforce-canonical-classes`](./enforce-canonical-classes.md) if both rules are enabled. It is recommended to use only one of them to avoid conflicting fixes.\n\n<br/>\n\n## Options\n\n### `position`\n\nControls where the important modifier (`!`) should be placed in class names.\n\n- `legacy`: Places the important modifier at the beginning of the class name.\n- `recommended`: Places the important modifier at the end of the class name.\n\n  **Type**: `\"legacy\" | \"recommended\"`  \n  **Default**: `\"recommended\"` in Tailwind CSS v4, `\"legacy\"` in Tailwind CSS v3 and earlier.  \n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).  \n  If not specified, the plugin will fall back to the default configuration.  \n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  The path to the `tailwind.config.js` file. If not specified, the plugin will try to find it automatically or falls back to the default configuration.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tailwindConfig).  \n\n  For Tailwind CSS v4 and the css based config, use the [`entryPoint`](#entrypoint) option instead.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tsconfig).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\n### Recommended Position\n\n```tsx\n// ❌ BAD: with option { \"position\": \"recommended\" }\n<div class=\"!text-red-500 hover:!bg-blue-500\"></div>;\n```\n\n```tsx\n// ✅ GOOD: with option { \"position\": \"recommended\" }\n<div class=\"text-red-500! hover:bg-blue-500!\"></div>;\n```\n\n### Legacy Position\n\n```tsx\n// ❌ BAD: with option { \"position\": \"legacy\" }\n<div class=\"text-red-500! hover:bg-blue-500!\"></div>;\n```\n\n```tsx\n// ✅ GOOD: with option { \"position\": \"legacy\" }\n<div class=\"!text-red-500 hover:!bg-blue-500\"></div>;\n```\n"
  },
  {
    "path": "docs/rules/enforce-consistent-line-wrapping.md",
    "content": "# better-tailwindcss/enforce-consistent-line-wrapping\n\nEnforce tailwind classes to be broken up into multiple lines. It is possible to break at a certain print width or a certain number of classes per line.\n\n<br/>\n\n## Options\n\n### `printWidth`\n\n  The maximum line length. Lines are wrapped appropriately to stay within this limit. The value `0` disables line wrapping by `printWidth`.\n  Tabs count according to [`tabWidth`](#tabwidth) when evaluating this limit.\n\n  **Type**: `number`  \n  **Default**: `80`\n\n<br/>\n\n### `classesPerLine`\n\n  The maximum amount of classes per line. Lines are wrapped appropriately to stay within this limit . The value `0` disables line wrapping by `classesPerLine`.\n\n  **Type**: `number`  \n  **Default**: `0`\n\n<br/>\n\n### `group`\n\n  Defines how different groups of classes should be separated. A group is a set of classes that share the same variant.\n\n  **Type**: `\"emptyLine\" | \"never\" | \"newLine\"`  \n  **Default**: `\"newLine\"`  \n\n<br/>\n\n### `preferSingleLine`\n\n  Prefer a single line for different variants. When set to `true`, the rule will keep all variants on a single line until the line exceeds the `printWidth` or `classesPerLine` limit.\n\n  **Type**: `boolean`  \n  **Default**: `false`  \n\n<br/>\n\n### `indent`\n\n  Determines how the code should be indented. A number defines the amount of space characters, and the string `\"tab\"` will use a single tab character.\n\n  **Type**: `number | \"tab\"`  \n  **Default**: `2`\n\n<br/>\n\n### `tabWidth`\n\n  Determines how many columns a tab character contributes when checking `printWidth`.\n  This option only affects width calculations and does not change emitted indentation characters.\n\n  **Type**: `number`  \n  **Default**: `1`\n\n<br/>\n\n### `lineBreakStyle`\n\n  The line break style.  \n  The style `windows` will use `\\r\\n` as line breaks and `unix` will use `\\n`.\n\n  **Type**: `\"windows\" | \"unix\"`  \n  **Default**: `\"unix\"`\n\n<br />\n\n### `strictness`\n\n  When used in combination with formatters like prettier, biome or oxfmt, the line wrapping might interfere with the line wrapping of those formatters in some [edge cases](https://github.com/schoero/eslint-plugin-better-tailwindcss/issues/243).  \n  If you experience such issues, you can set the `strictness` option to `\"loose\"` to make the rule less strict about line wrapping.\n  This will allow the lines to slightly exceed the `printWidth` if the plugin detects that the line wrapping would likely cause conflicts with a formatter.\n\n  **Type**: `\"strict\" | \"loose\"`  \n  **Default**: `\"strict\"`\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).  \n  If not specified, the plugin will fall back to the default configuration.  \n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  The path to the `tailwind.config.js` file. If not specified, the plugin will try to find it automatically or falls back to the default configuration.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tailwindConfig).  \n\n  For Tailwind CSS v4 and the css based config, use the [`entryPoint`](#entrypoint) option instead.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tsconfig).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\nWith the default options, a class name will be broken up into multiple lines and grouped by their variants. Groups are separated by an empty line.  \n\nThe following examples show how the rule behaves with different options:\n\n```tsx\n// ❌ BAD\n<div class=\"text-black underline focus:font-bold focus:text-opacity-70 hover:font-bold hover:text-opacity-70\" />;\n```\n\n```tsx\n// ✅ GOOD: with option { group: 'emptyLine' }\n<div class={`\n  text-black underline\n\n  focus:font-bold focus:text-opacity-70\n\n  hover:font-bold hover:text-opacity-70\n`} />;\n```\n\n```tsx\n// ✅ GOOD: with option { group: 'newLine' }\n<div class={`\n  text-black underline\n  focus:font-bold focus:text-opacity-70\n  hover:font-bold hover:text-opacity-70\n`} />;\n```\n\n```tsx\n// ✅ GOOD: with option { group: 'never', printWidth: 80 }\n<div class={`\n  text-black underline focus:font-bold focus:text-opacity-70 hover:font-bold\n  hover:text-opacity-70\n`} />;\n```\n\n```tsx\n// ✅ GOOD: with { classesPerLine: 1, group: 'emptyLine' }\n<div class={`\n  text-black\n  underline\n\n  focus:font-bold\n  focus:text-opacity-70\n\n  hover:font-bold\n  hover:text-opacity-70\n`} />;\n```\n\n```tsx\n// ✅ GOOD: with { group: \"newLine\", preferSingleLine: true, printWidth: 120 }\n<div class=\"text-black underline focus:font-bold focus:text-opacity-70 hover:font-bold hover:text-opacity-70\" />;\n```\n\n```tsx\n// ✅ GOOD: with { group: \"newLine\", preferSingleLine: true, printWidth: 80 }\n<div class={`\n  text-black underline\n  focus:font-bold focus:text-opacity-70\n  hover:font-bold hover:text-opacity-70\n`} />;\n```\n"
  },
  {
    "path": "docs/rules/enforce-consistent-variable-syntax.md",
    "content": "# better-tailwindcss/enforce-consistent-variable-syntax\n\nEnforce consistent css variable syntax in Tailwind CSS class strings.\n\n<br/>\n\n> [!NOTE]\n> This rule might interfere with [`better-tailwindcss/enforce-canonical-classes`](./enforce-canonical-classes.md) if both rules are enabled. It is recommended to use only one of them to avoid conflicting fixes.\n\n<br/>\n\n## Options\n\n### `syntax`\n\n  The syntax to enforce for css variables in Tailwind CSS class strings.\n\n  The `shorthand` syntax uses the `(--variable)` syntax in Tailwind CSS v4 and `[--variable]` syntax in Tailwind CSS v3.\n\n  **Type**: `\"variable\"` | `\"shorthand\"`  \n  **Default**: `\"shorthand\"`\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: Incorrect css variable syntax with option `syntax: \"shorthand\"`\n<div class=\"bg-[var(--primary)]\" />;\n```\n\n```tsx\n// ✅ GOOD: With option `syntax: \"shorthand\"` in Tailwind CSS v4\n<div class=\"bg-(--primary)\" />;\n```\n\n```tsx\n// ✅ GOOD: With option `syntax: \"shorthand\"` in Tailwind CSS v3\n<div class=\"bg-[--primary]\" />;\n```\n\n```tsx\n// ❌ BAD: Incorrect css variable syntax with option `syntax: \"variable\"` in Tailwind CSS v4\n<div class=\"bg-(--primary)\" />;\n```\n\n```tsx\n// ❌ BAD: Incorrect css variable syntax with option `syntax: \"variable\"` in Tailwind CSS v3\n<div class=\"bg-[--primary]\" />;\n```\n\n```tsx\n// ✅ GOOD: With option `syntax: \"variable\"`\n<div class=\"bg-[var(--primary)]\" />;\n```\n"
  },
  {
    "path": "docs/rules/enforce-consistent-variant-order.md",
    "content": "# better-tailwindcss/enforce-consistent-variant-order\n\nEnforce Tailwind CSS variant order for class names.\n\n<br/>\n\n## Options\n\nThis rule has no custom options.\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: variants are not in Tailwind's recommended order\n<div class=\"hover:lg:text-red-500\" />;\n```\n\n```tsx\n// ✅ GOOD: variants follow Tailwind's recommended order\n<div class=\"lg:hover:text-red-500\" />;\n```\n\n<br/>\n\n> [!NOTE]\n> This rule only enforces variant order for Tailwind CSS v4 projects.\n"
  },
  {
    "path": "docs/rules/enforce-logical-properties.md",
    "content": "# better-tailwindcss/enforce-logical-properties\n\nEnforce logical utility class names over physical directions for better RTL and LTR support.\n\nThe rule reports physical classes and auto-fixes them to their logical equivalent when a direct Tailwind replacement exists.\n\n## Physical to Logical Mappings\n\n| **Physical** | **Logical** |\n| :--- | :--- |\n| `pl-*` | `ps-*` |\n| `pr-*` | `pe-*` |\n| `pt-*` | `pbs-*` |\n| `pb-*` | `pbe-*` |\n| `ml-*` | `ms-*` |\n| `mr-*` | `me-*` |\n| `mt-*` | `mbs-*` |\n| `mb-*` | `mbe-*` |\n| `scroll-pl-*` | `scroll-ps-*` |\n| `scroll-pr-*` | `scroll-pe-*` |\n| `scroll-pt-*` | `scroll-pbs-*` |\n| `scroll-pb-*` | `scroll-pbe-*` |\n| `scroll-ml-*` | `scroll-ms-*` |\n| `scroll-mr-*` | `scroll-me-*` |\n| `scroll-mt-*` | `scroll-mbs-*` |\n| `scroll-mb-*` | `scroll-mbe-*` |\n| `left-*` | `inset-s-*` |\n| `right-*` | `inset-e-*` |\n| `top-*` | `inset-bs-*` |\n| `bottom-*` | `inset-be-*` |\n| `border-l` / `border-l-*` | `border-s` / `border-s-*` |\n| `border-r` / `border-r-*` | `border-e` / `border-e-*` |\n| `border-t` / `border-t-*` | `border-bs` / `border-bs-*` |\n| `border-b` / `border-b-*` | `border-be` / `border-be-*` |\n| `rounded-l` / `rounded-l-*` | `rounded-s` / `rounded-s-*` |\n| `rounded-r` / `rounded-r-*` | `rounded-e` / `rounded-e-*` |\n| `rounded-tl` / `rounded-tl-*` | `rounded-ss` / `rounded-ss-*` |\n| `rounded-tr` / `rounded-tr-*` | `rounded-se` / `rounded-se-*` |\n| `rounded-br` / `rounded-br-*` | `rounded-ee` / `rounded-ee-*` |\n| `rounded-bl` / `rounded-bl-*` | `rounded-es` / `rounded-es-*` |\n| `text-left` | `text-start` |\n| `text-right` | `text-end` |\n| `float-left` | `float-start` |\n| `float-right` | `float-end` |\n| `clear-left` | `clear-start` |\n| `clear-right` | `clear-end` |\n| `h-*` | `block-*` |\n| `w-*` | `inline-*` |\n| `min-h-*` | `min-block-*` |\n| `min-w-*` | `min-inline-*` |\n| `max-h-*` | `max-block-*` |\n| `max-w-*` | `max-inline-*` |\n| `size-*` | `block-* inline-*` |\n\n<br/>\n\n## Options\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [settings object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).\n  If not specified, the plugin will fall back to the default configuration.\n\n  **Type**: `string`\n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  Tailwind config file path.\n\n  **Type**: string\n  **Default**: Tailwind's default config resolution\n\n  <br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.\n  This can also be set globally via the [settings object](../settings/settings.md#tsconfig).\n\n  The tsconfig is used to resolve tsconfig [path](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`\n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: physical direction classes\n<div class=\"pl-4 pt-2 mr-2 mt-1 text-left rounded-tr-lg right-0 top-0 border-t\" />;\n```\n\n```tsx\n// ✅ GOOD: logical direction classes\n<div class=\"ps-4 pbs-2 me-2 mbs-1 text-start rounded-se-lg inset-e-0 inset-bs-0 border-bs\" />;\n```\n"
  },
  {
    "path": "docs/rules/enforce-shorthand-classes.md",
    "content": "# better-tailwindcss/enforce-shorthand-classes\n\nThis rule identifies when multiple longhand Tailwind CSS classes can be replaced with a single shorthand class, improving code readability and reducing bundle size.\n\n<br/>\n\n> [!NOTE]\n> This rule might interfere with [`better-tailwindcss/enforce-canonical-classes`](./enforce-canonical-classes.md) if both rules are enabled. It is recommended to use only one of them to avoid conflicting fixes.\n\n<br/>\n\n## Options\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: using separate padding classes\n<div class=\"pt-4 pr-4 pb-4 pl-4\" />;\n```\n\n```tsx\n// ✅ GOOD: using shorthand padding class\n<div class=\"p-4\" />;\n```\n\n```tsx\n// ❌ BAD: using separate width and height classes\n<div class=\"w-4 h-4\" />;\n```\n\n```tsx\n// ✅ GOOD: using shorthand size class\n<div class=\"size-4\" />;\n```\n"
  },
  {
    "path": "docs/rules/no-conflicting-classes.md",
    "content": "# better-tailwindcss/no-conflicting-classes\n\nDisallow conflicting classes in Tailwind CSS class strings. Conflicting classes are classes that apply the same CSS property on the same element. This can cause unexpected behavior as it is not clear which class will take precedence.\n\n<br/>\n\n> [!NOTE]\n> This rule is similar to `cssConflict` from the [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) VSCode extension. It is recommended to disable `cssConflict` in your projects `.vscode/settings.json` to avoid confusion:\n>\n> ```jsonc\n> {\n>   \"tailwindCSS.lint.cssConflict\": \"ignore\"\n> }\n> ```\n\n<br/>\n\n## Options\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).  \n  If not specified, the plugin will fall back to the default configuration.  \n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  The path to the `tailwind.config.js` file. If not specified, the plugin will try to find it automatically or falls back to the default configuration.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tailwindConfig).  \n\n  For Tailwind CSS v4 and the css based config, use the [`entryPoint`](#entrypoint) option instead.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tsconfig).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: Conflicting class detected: \"flex\" and \"grid\" apply the same css properties: \"display\"\n<div class=\"flex grid\" />;\n```\n\n```tsx\n// ✅ GOOD: no conflicting classes\n<div class=\"flex w-full\" />;\n```\n"
  },
  {
    "path": "docs/rules/no-deprecated-classes.md",
    "content": "# better-tailwindcss/no-deprecated-classes\n\nDisallow the use of [deprecated Tailwind CSS classes](https://tailwindcss.com/docs/upgrade-guide#removed-deprecated-utilities) in Tailwind CSS v4.\n\nThe following classes will be reported as deprecated:\n\n## Deprecated Utilities\n\n| **Deprecated**             | **Replacement**                               |\n|---------------------------|-----------------------------------------------|\n| `bg-opacity-*`            | Use opacity modifiers like `bg-black/50`      |\n| `text-opacity-*`          | Use opacity modifiers like `text-black/50`    |\n| `border-opacity-*`        | Use opacity modifiers like `border-black/50`  |\n| `divide-opacity-*`        | Use opacity modifiers like `divide-black/50`  |\n| `ring-opacity-*`          | Use opacity modifiers like `ring-black/50`    |\n| `placeholder-opacity-*`   | Use opacity modifiers like `placeholder-black/50` |\n| `flex-shrink`             | `shrink`                                      |\n| `flex-shrink-*`           | `shrink-*`                                    |\n| `flex-grow`               | `grow`                                        |\n| `flex-grow-*`             | `grow-*`                                      |\n| `overflow-ellipsis`       | `text-ellipsis`                               |\n| `decoration-slice`        | `box-decoration-slice`                        |\n| `decoration-clone`        | `box-decoration-clone`                        |\n\n## Renamed Utilities (v3 → v4)\n\n| **v3**                    | **v4**                   |\n|--------------------------|--------------------------|\n| `shadow`                 | `shadow-sm`              |\n| `drop-shadow`            | `drop-shadow-sm`         |\n| `blur`                   | `blur-sm`                |\n| `backdrop-blur`          | `backdrop-blur-sm`       |\n| `rounded`                | `rounded-sm`             |\n\n<br/>\n\n## Options\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).  \n  If not specified, the plugin will fall back to the default configuration.  \n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  Tailwind config file path.  \n  \n  **Type**: string  \n  **Default**: Tailwind's default config resolution\n\n  <br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tsconfig).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: using deprecated shadow class\n<div class=\"shadow\" />;\n```\n\n```tsx\n// ✅ GOOD: using updated shadow class\n<div class=\"shadow-sm\" />;\n```\n\n```tsx\n// ❌ BAD: using deprecated flex-shrink class\n<div class=\"flex-shrink-1\" />;\n```\n\n```tsx\n// ✅ GOOD: using updated shrink class\n<div class=\"shrink-1\" />;\n```\n"
  },
  {
    "path": "docs/rules/no-duplicate-classes.md",
    "content": "# better-tailwindcss/no-duplicate-classes\n\nDisallow duplicate classes in Tailwind CSS class strings.\n\n<br/>\n\n## Options\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: duplicate classes\n<div class=\"rounded underline rounded\" />;\n```\n\n```tsx\n// ✅ GOOD: no duplicate classes\n<div class=\"rounded underline\" />;\n```\n\n<br/>\n\n> [!NOTE]\n> This rule is smart. It is able to detect duplicates across template literal boundaries.\n\n```tsx\n// ❌ BAD: duplicate classes in conditional template literal classes and around template elements\n<div class={`\n  underline italic\n  ${someCondition === true ? \"rounded  underline font-bold\" : \"rounded underline font-thin\"}\n  italic\n`} />;\n```\n\n```tsx\n// ✅ GOOD: no duplicate classes\n<div class={`\n  underline italic\n  ${someCondition === true ? \"rounded  font-bold\" : \"rounded font-thin\"}\n`} />;\n```\n"
  },
  {
    "path": "docs/rules/no-restricted-classes.md",
    "content": "# better-tailwindcss/no-restricted-classes\n\nDisallow the usage of certain classes. This can be useful to disallow classes that are not recommended to be used in your project. For example, you can enforce the use of semantic color names or disallow features like arbitrary values (text-[#fff]), child variants (`*:`) or the `!important` modifier (`!`) in your project.\n\nIt is also possible to provide a custom error message and a fix for the disallowed class. The fix can be used to automatically replace the disallowed class with a recommended one.\n\n<br/>\n\n## Options\n\n### `restrict`\n\n  The classes that should be disallowed. The patterns in this list are treated as regular expressions.\n  Matched groups of the regular expression can be used in the error message or fix by using the `$1`, `$2`, etc. syntax.\n\n  **Type**: `string[] | { pattern: string, message?: string, fix?: string }[]`  \n  **Default**: `[]`\n\n  Make sure to match possible variants and modifiers of the class names as well:\n\n  ```json\n  {\n    \"restrict\": [{\n      \"fix\": \"$1$2-success$3\",\n      \"message\": \"Restricted class: Use '$1$2-success$3' instead.\",\n      \"pattern\": \"^([a-zA-Z0-9:/_-]*:)?(text|bg)-green-500(\\\\/[0-9]{1,3})?$\"\n    }]\n  }\n  ```\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: disallow the use of the `text-green-500` class with option `{ restrict: [{ pattern: \"^(.*)-green-500$\", message: \"Restricted class: Use '$1-success' instead.\" }] }`\n<div class=\"rounded text-green-500\" />;\n//                  ~~~~~~~~~~~~~~ Restricted class: Use 'text-success' instead.\n```\n\n```tsx\n// ❌ BAD: disallow the use of the arbitrary values with option `{ restrict: [\"\\\\[([^\\\\[\\\\]]*?)\\\\](?!:)\"] }`\n<div class=\"rounded text-[#fff]\" />;\n//                       ~~~~~~\n```\n\n```tsx\n// ❌ BAD: disallow the use of the child variants with option `{ restrict: [\"^\\\\*+:.*\"] }`\n<div class=\"rounded *:mx-0\" />;\n//                  ~~~~~~\n```\n\n```tsx\n// ❌ BAD: disallow the use of the important modifier with option `{ restrict: [\"^.*!$\"] }`\n<div class=\"rounded p-4!\" />;\n//                  ~~~~\n```\n\n```tsx\n// ❌ BAD: disallow the use of unnamed groups with option `{ restrict: [\"^group$\"] }`\n<div class=\"group\" />;\n//          ~~~~~\n```\n\n```tsx\n// ❌ BAD: disallow the use of unnamed group variants with option `{ restrict: [\"^group-(hover|focus|active|visited|disabled):\"] }`\n<div class=\"group-hover:p-4\" />;\n//          ~~~~~~~~~~~\n```\n"
  },
  {
    "path": "docs/rules/no-unknown-classes.md",
    "content": "# better-tailwindcss/no-unknown-classes\n\nDisallow unknown classes in Tailwind CSS class strings. Unknown classes are classes that are not defined in your Tailwind CSS config file and therefore not recognized by Tailwind CSS.\n\n<br/>\n\n## Options\n\n### `ignore`\n\n  List of List of regex patterns for classes that should not report an error.\n  \n  **Type**: `string[]`  \n  **Default**: `[]`\n\n<br/>\n\n### `detectComponentClasses`\n\n  Tailwind CSS v4 allows you to define custom [component classes](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes) like `card`, `btn`, `badge` etc.\n  \n  If you want to create such classes, you can set this option to `true` to allow the rule to detect those classes and not report them as unknown classes. This can also be configured via the [`settings` object](../settings/settings.md).\n  \n  **Type**: `boolean`  \n  **Default**: `false`\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n  <br/>\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`).  \n  If not specified, the plugin will fall back to the default configuration.  \n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n  <br/>\n\n### `tailwindConfig`\n\n  The path to the `tailwind.config.js` file. If not specified, the plugin will try to find it automatically or falls back to the default configuration.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tailwindConfig).  \n\n  For Tailwind CSS v4 and the css based config, use the [`entryPoint`](#entrypoint) option instead.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  This can also be set globally via the [`settings` object](../settings/settings.md#tsconfig).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: unknown class\n<div class=\"my-class\" />;\n```\n\n```tsx\n// ✅ GOOD: only valid tailwindcss classes\n<div class=\"font-bold hover:underline\" />;\n```\n"
  },
  {
    "path": "docs/rules/no-unnecessary-whitespace.md",
    "content": "# better-tailwindcss/no-unnecessary-whitespace\n\nDisallow unnecessary whitespace in between and around tailwind classes.\n\n<br/>\n\n## Options\n\n### `allowMultiline`\n\n  Allow multi-line class declarations.  \n  If this option is disabled, template literal strings will be collapsed into a single line string wherever possible. Must be set to `true` when used in combination with [better-tailwindcss/enforce-consistent-line-wrapping](./enforce-consistent-line-wrapping.md).  \n  \n  **Type**: `boolean`  \n  **Default**: `true`\n\n<br/>\n\n<br/>\n\n<details>\n  <summary>Common options</summary>\n\n  <br/>\n\n  These options are common to all rules and can also be set globally via the [`settings` object](../settings/settings.md).\n\n  <br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)\n  **Default**: See [defaults API](../api/defaults.md)\n\n</details>\n\n<br/>\n\n## Examples\n\n```tsx\n// ❌ BAD: random unnecessary whitespace\n<div class=\" text-black    underline  hover:text-opacity-70   \" />;\n```\n\n```tsx\n// ✅ GOOD: only necessary whitespace is remaining\n<div class=\"text-black underline hover:text-opacity-70\"/>;\n```\n"
  },
  {
    "path": "docs/settings/settings.md",
    "content": "# Settings\n\n## Table of Contents\n\n- [entryPoint](#entrypoint)\n- [tailwindConfig](#tailwindconfig)\n- [tsconfig](#tsconfig)\n- [cwd](#cwd)\n- [detectComponentClasses](#detectcomponentclasses)\n- [rootFontSize](#rootfontsize)\n- [messageStyle](#messagestyle)\n- [selectors](#selectors)\n\n<br />\n<br />\n\nThe settings object can be used to globally configure shared options across all rules. Global options will always be overridden by rule-specific options.\nTo set the settings object, add a `settings` key to the eslint config.\n\n<br />\n<br />\n\n```jsonc\n// eslint.config.js\n{\n  // \"plugins\": {... },\n  // \"rules\": { ... },\n  \"settings\": {\n    \"better-tailwindcss\": {\n      // ...\n    }\n  }\n}\n```\n\n<br />\n<br />\n\n### `entryPoint`\n\n  The path to the entry file of the css based tailwind config (eg: `src/global.css`). If not specified, the plugin will fall back to the default configuration. Relative to the [current working directory](#cwd).  \n  The tailwind config is used for various rules.\n\n  **Type**: `string`\n\n<br/>\n\n### `tailwindConfig`\n\n  The path to the `tailwind.config.js` file. If not specified, the plugin will try to find it automatically or falls back to the default configuration. Relative to the [current working directory](#cwd).  \n  The tailwind config is used for various rules.\n\n  For Tailwind CSS v4 and the css based config, use the [`entryPoint`](#entrypoint) option instead.\n\n  **Type**: `string`\n  \n<br/>\n\n### `tsconfig`\n\n  The path to the `tsconfig.json` file. If not specified, the plugin will try to find it automatically.  \n  Relative to the [current working directory](#cwd).  \n\n  The tsconfig is used to resolve tsconfig [`path`](https://www.typescriptlang.org/tsconfig/#paths) aliases.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `cwd`\n\n  The working directory used to resolve `tailwindcss` and related config files. This is useful for monorepos where linting runs from the repository root but each project has its own `node_modules` and Tailwind setup.\n\n  This path is resolved relative to the current working directory of the ESLint process. If not specified, it falls back to the current working directory of the ESLint process.\n\n  **Type**: `string`  \n  **Default**: `undefined`\n\n<br/>\n\n### `detectComponentClasses`\n\n  Tailwind CSS v4 allows you to define custom [component classes](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes) like `card`, `btn`, `badge` etc.\n  \n  If you want to create such classes, you can set this option to `true` to allow the rule to detect those classes and not report them as unknown classes.\n  \n  **Type**: `boolean`  \n  **Default**: `false`\n\n<br/>\n\n### `rootFontSize`\n\nThe font size of the `<html>` element in pixels. By default, the root font size is `16px` unless it is changed with CSS.\nIf provided, this will be used to determine if arbitrary values can be replaced with predefined sizing scales.\n\n**Type**: `number | undefined`  \n**Default**: `undefined`\n\n<br/>\n\n### `messageStyle`\n\n  Customize how linting messages are displayed.\n  \n  `\"visual\"` visualizes whitespaces and line breaks for better readability.  \n  `\"compact\"` displays visual message on a single line, better suitable for CI environments.  \n  `\"raw\"` shows only the raw information without whitespace or line break visualization.  \n\n  **Type**: `\"visual\" | \"compact\" | \"raw\"`  \n  **Default**: `\"visual\"`, `\"compact\"` in CI environments\n\n<br/>\n\n### `selectors`\n\n  Flat list of selectors that determines where Tailwind class strings are linted.\n\n  This controls what gets linted globally: only string literals matched by these selectors are treated as Tailwind class candidates.\n\n  **Type**: Array of [Selectors](../configuration/advanced.md#selectors)  \n  **Default**: See [defaults API](../api/defaults.md)\n"
  },
  {
    "path": "eslint.config.ts",
    "content": "import config from \"@schoero/configs/eslint\";\nimport { defineConfig } from \"eslint/config\";\n\n\nexport default defineConfig([\n  ...config,\n  {\n    files: [\"**/*.test.{js,jsx,cjs,mjs,ts,tsx}\", \"**/*.test-d.{ts,tsx}\"],\n    rules: {\n      \"eslint-plugin-stylistic/quotes\": [\"warn\", \"double\", { allowTemplateLiterals: \"always\", avoidEscape: true }],\n      \"eslint-plugin-typescript/no-unnecessary-condition\": \"off\",\n      \"eslint-plugin-typescript/no-useless-template-literals\": \"off\",\n      \"eslint-plugin-vitest/expect-expect\": \"off\"\n    }\n  },\n  {\n    files: [\"**/*.test.ts\"],\n    rules: {\n      \"eslint-plugin-perfectionist/sort-objects\": [\n        \"warn\",\n        {\n          customGroups: [\n            {\n              elementNamePattern: \"^(astro|angular|jsx|svelte|vue|html)(Output)?$\",\n              groupName: \"markup\",\n              selector: \"property\"\n            }\n          ],\n          groups: [\"markup\", { newlinesBetween: \"always\" }, \"unknown\"],\n          ignoreCase: true,\n          partitionByComment: false,\n          type: \"alphabetical\"\n        }\n      ],\n      \"eslint-plugin-typescript/naming-convention\": \"off\",\n      \"eslint-plugin-typescript/no-floating-promises\": \"off\"\n\n    }\n  },\n  {\n    files: [\"tests/utils/lint.ts\"],\n    rules: {\n      \"eslint-plugin-typescript/naming-convention\": \"off\"\n    }\n  }\n]);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"version\": \"4.5.0\",\n  \"type\": \"module\",\n  \"name\": \"eslint-plugin-better-tailwindcss\",\n  \"description\": \"auto-wraps tailwind classes after a certain print width or class count into multiple lines to improve readability.\",\n  \"license\": \"MIT\",\n  \"author\": \"Roger Schönbächler\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/schoero/eslint-plugin-better-tailwindcss.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/schoero/eslint-plugin-better-tailwindcss/issues\"\n  },\n  \"exports\": {\n    \".\": \"./lib/configs/config.js\",\n    \"./api/defaults\": \"./lib/api/defaults.js\",\n    \"./api/types\": \"./lib/api/types.js\",\n    \"./defaults\": \"./lib/api/defaults.js\",\n    \"./types\": \"./lib/api/types.js\"\n  },\n  \"main\": \"./lib/configs/config.js\",\n  \"scripts\": {\n    \"build\": \"vite-node build\",\n    \"build:ci\": \"vite-node build\",\n    \"eslint\": \"eslint .\",\n    \"eslint:ci\": \"npm run eslint -- --max-warnings 0\",\n    \"eslint:fix\": \"npm run eslint -- --fix\",\n    \"install:v3\": \"npm i tailwindcss@^3 --no-save\",\n    \"install:v4\": \"npm i tailwindcss@^4 --no-save\",\n    \"lint\": \"npm run eslint && npm run markdownlint\",\n    \"lint:ci\": \"npm run eslint:ci && npm run markdownlint:ci\",\n    \"lint:fix\": \"npm run eslint:fix && npm run markdownlint:fix\",\n    \"markdownlint\": \"markdownlint-cli2 '**/*.md' '#**/node_modules'\",\n    \"markdownlint:ci\": \"npm run markdownlint\",\n    \"markdownlint:fix\": \"npm run markdownlint -- --fix\",\n    \"postrelease:alpha\": \"eslint --fix package.json && markdownlint-cli2 --fix 'CHANGELOG.md'\",\n    \"postrelease:beta\": \"eslint --fix package.json && markdownlint-cli2 --fix 'CHANGELOG.md'\",\n    \"postrelease:latest\": \"eslint --fix package.json && markdownlint-cli2 --fix 'CHANGELOG.md'\",\n    \"prebuild\": \"npm run typecheck && npm run lint && npm run spellcheck\",\n    \"prerelease:alpha\": \"npm run test -- --run && npm run build\",\n    \"prerelease:beta\": \"npm run test -- --run && npm run build\",\n    \"prerelease:latest\": \"npm run test -- --run && npm run build\",\n    \"pretest:v3\": \"npm run install:v3\",\n    \"pretest:v4\": \"npm run install:v4\",\n    \"publish:alpha\": \"changelogen gh release && changelogen --publish --publishTag alpha\",\n    \"publish:beta\": \"changelogen gh release && changelogen --publish --publishTag beta\",\n    \"publish:latest\": \"changelogen gh release && changelogen --publish\",\n    \"release:alpha\": \"changelogen --bump --output --prerelease alpha\",\n    \"release:beta\": \"changelogen --bump --output --prerelease beta\",\n    \"release:latest\": \"changelogen --bump --output --no-tag\",\n    \"spellcheck\": \"cspell lint\",\n    \"spellcheck:ci\": \"npm run spellcheck -- --no-progress\",\n    \"test\": \"vitest -c ./vite.config.ts --exclude tests/e2e\",\n    \"test:all\": \"npm run test:v3 && npm run test:v4\",\n    \"test:e2e\": \"vitest -c ./vite.config.ts tests/e2e --run\",\n    \"test:v3\": \"npm run test -- --run\",\n    \"test:v4\": \"npm run test -- --run\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"engines\": {\n    \"node\": \"^20.19.0 || ^22.12.0 || >=23.0.0\"\n  },\n  \"files\": [\n    \"lib\"\n  ],\n  \"peerDependencies\": {\n    \"eslint\": \"^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0\",\n    \"oxlint\": \"^1.35.0\",\n    \"tailwindcss\": \"^3.3.0 || ^4.1.17\"\n  },\n  \"peerDependenciesMeta\": {\n    \"eslint\": {\n      \"optional\": true\n    },\n    \"oxlint\": {\n      \"optional\": true\n    }\n  },\n  \"dependencies\": {\n    \"@eslint/css-tree\": \"^4.0.1\",\n    \"@valibot/to-json-schema\": \"^1.6.0\",\n    \"enhanced-resolve\": \"^5.20.1\",\n    \"jiti\": \"^2.6.1\",\n    \"synckit\": \"^0.11.12\",\n    \"tailwind-csstree\": \"^0.3.0\",\n    \"tsconfig-paths-webpack-plugin\": \"^4.2.0\",\n    \"valibot\": \"^1.3.1\"\n  },\n  \"devDependencies\": {\n    \"@angular/compiler\": \"^21.2.6\",\n    \"@angular-eslint/template-parser\": \"^21.3.1\",\n    \"@eslint/css\": \"^1.1.0\",\n    \"@html-eslint/parser\": \"0.58.1\",\n    \"@oxc-node/core\": \"^0.0.35\",\n    \"@schoero/configs\": \"^1.5.32\",\n    \"@types/estree-jsx\": \"^1.0.5\",\n    \"@types/node\": \"^25.5.0\",\n    \"@typescript-eslint/parser\": \"^8.58.0\",\n    \"astro-eslint-parser\": \"^1.4.0\",\n    \"changelogen\": \"^0.6.2\",\n    \"cspell\": \"^10.0.0\",\n    \"daisyui\": \"^5.5.19\",\n    \"es-html-parser\": \"^0.3.1\",\n    \"eslint\": \"^9.39.4\",\n    \"eslint-plugin-better-tailwindcss\": \"file:./\",\n    \"json-schema\": \"^0.4.0\",\n    \"markdownlint\": \"^0.40.0\",\n    \"oxlint\": \"^1.58.0\",\n    \"prettier\": \"^3.8.1\",\n    \"svelte\": \"^5.55.1\",\n    \"svelte-eslint-parser\": \"^1.6.0\",\n    \"tailwindcss\": \"^4.2.2\",\n    \"tsc-alias\": \"^1.8.16\",\n    \"typescript\": \"^5.9.3\",\n    \"vite-node\": \"^6.0.0\",\n    \"vitest\": \"^4.1.2\",\n    \"vue-eslint-parser\": \"^10.4.0\"\n  },\n  \"keywords\": [\n    \"eslint\",\n    \"eslint-plugin\",\n    \"stylistic\",\n    \"formatting\",\n    \"tailwind\",\n    \"readable\",\n    \"readability\",\n    \"horizontal\",\n    \"scrolling\",\n    \"multiline\",\n    \"multi\",\n    \"newline\",\n    \"line\",\n    \"break\",\n    \"linebreak\",\n    \"wrap\",\n    \"template\",\n    \"literal\",\n    \"jsx\",\n    \"html\",\n    \"astro\",\n    \"svelte\",\n    \"vue\",\n    \"react\",\n    \"qwik\",\n    \"solid\",\n    \"template-literal\",\n    \"template-literals\",\n    \"tailwindcss\",\n    \"tailwind-css\",\n    \"tailwind-classes\"\n  ],\n  \"volta\": {\n    \"node\": \"25.2.1\"\n  }\n}\n"
  },
  {
    "path": "src/api/defaults.ts",
    "content": "/* eslint-disable eslint-plugin-jsdoc/require-returns */\n/* eslint-disable eslint-plugin-jsdoc/require-description */\nimport { DEFAULT_SELECTORS } from \"better-tailwindcss:options/default-options.js\";\nimport { migrateFlatSelectorsToLegacySelectors } from \"better-tailwindcss:options/migrate.js\";\nimport { SelectorKind } from \"better-tailwindcss:types/rule.js\";\nimport { isSelectorKind } from \"better-tailwindcss:utils/selectors.js\";\n\nimport type { Attributes } from \"better-tailwindcss:options/schemas/attributes.js\";\nimport type { Callees } from \"better-tailwindcss:options/schemas/callees.js\";\nimport type { Tags } from \"better-tailwindcss:options/schemas/tags.js\";\nimport type { Variables } from \"better-tailwindcss:options/schemas/variables.js\";\n\n/**\n * @deprecated Migrate to selectors instead.\n */\nexport function getDefaultCallees() {\n  return migrateFlatSelectorsToLegacySelectors(\n    DEFAULT_SELECTORS.filter(isSelectorKind(SelectorKind.Callee))\n  ).callees ?? [] satisfies Callees;\n}\n\n/**\n * @deprecated Migrate to selectors instead.\n */\nexport function getDefaultAttributes() {\n  return migrateFlatSelectorsToLegacySelectors(\n    DEFAULT_SELECTORS.filter(isSelectorKind(SelectorKind.Attribute))\n  ).attributes ?? [] satisfies Attributes;\n}\n\n/**\n * @deprecated Migrate to selectors instead.\n */\nexport function getDefaultVariables() {\n  return migrateFlatSelectorsToLegacySelectors(\n    DEFAULT_SELECTORS.filter(isSelectorKind(SelectorKind.Variable))\n  ).variables ?? [] satisfies Variables;\n}\n\n/**\n * @deprecated Migrate to selectors instead.\n */\nexport function getDefaultTags() {\n  return migrateFlatSelectorsToLegacySelectors(\n    DEFAULT_SELECTORS.filter(isSelectorKind(SelectorKind.Tag))\n  ).tags ?? [] satisfies Tags;\n}\n\nexport function getDefaultSelectors() {\n  return DEFAULT_SELECTORS;\n}\n"
  },
  {
    "path": "src/api/types.ts",
    "content": "/* Targets for arguments and calls */\nexport type { ArgumentTarget, CallTarget } from \"better-tailwindcss:types/rule.js\";\n\n/* Legacy Matchers */\nexport { MatcherType } from \"better-tailwindcss:types/rule.js\";\nexport type { Matcher, ObjectKeyMatcher, ObjectValueMatcher, StringMatcher } from \"better-tailwindcss:types/rule.js\";\n\n/* Selector */\nexport { SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nexport type { AttributeSelector, CalleeSelector, Selector, TagSelector, VariableSelector } from \"better-tailwindcss:types/rule.js\";\nexport type { SelectorAnonymousFunctionReturnMatcher, SelectorMatcher, SelectorObjectKeyMatcher, SelectorObjectValueMatcher, SelectorStringMatcher } from \"better-tailwindcss:types/rule.js\";\n\n/* Other types */\nexport type { Regex, Selectors } from \"better-tailwindcss:types/rule.js\";\n"
  },
  {
    "path": "src/async-utils/cache.ts",
    "content": "import { getModifiedDate } from \"./fs.js\";\n\n\ninterface CacheItem {\n  date: Date;\n  value: any;\n}\n\nconst CACHE = new Map<string, CacheItem>();\n\nexport function invalidateByModifiedDate(cache: CacheItem, path: string | undefined): boolean {\n  if(!path){ return true; }\n\n  const modified = getModifiedDate(path);\n  return modified > cache.date;\n}\n\nexport function withCache<Result>(key: string, path: string | undefined, callback: () => Result, invalidate?: (cache: CacheItem, path: string | undefined) => boolean): Result;\nexport function withCache<Result>(key: string, path: string | undefined, callback: () => Promise<Result>, invalidate?: (cache: CacheItem, path: string | undefined) => boolean): Promise<Result>;\nexport function withCache<Result>(key: string, path: string | undefined, callback: () => Promise<Result> | Result, invalidate: (cache: CacheItem, path: string | undefined) => boolean = invalidateByModifiedDate): Promise<Result> | Result {\n  const cacheKey = `${key}-${path}`;\n  const cached = CACHE.get(cacheKey);\n\n  if(cached && !invalidate(cached, path)){\n    return cached.value;\n  }\n\n  const value = callback();\n\n  if(value instanceof Promise){\n    return value.then(resolvedValue => {\n      CACHE.set(cacheKey, { date: new Date(), value: resolvedValue });\n      return resolvedValue;\n    });\n  } else {\n    CACHE.set(cacheKey, { date: new Date(), value });\n    return value;\n  }\n}\n\nexport function clearCache() {\n  CACHE.clear();\n}\n"
  },
  {
    "path": "src/async-utils/escape.ts",
    "content": "import { getCachedRegex } from \"./regex.js\";\n\n\nexport function escapeForRegex(word: string) {\n  return word.replace(getCachedRegex(/[$()*+./?[\\\\\\]^{|}-]/g), \"\\\\$&\");\n}\n"
  },
  {
    "path": "src/async-utils/fs.ts",
    "content": "import { existsSync, statSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\n\n\nexport function findPathRecursive(cwd: string, startDirectory: string, entries: string[]): string | undefined {\n  const resolvedPaths = entries.map(p => resolve(startDirectory, p));\n\n  for(let resolvedPath = resolvedPaths.shift(); resolvedPath !== undefined; resolvedPath = resolvedPaths.shift()){\n\n    if(existsSync(resolvedPath)){\n      return resolvedPath;\n    }\n\n    const fileName = basename(resolvedPath);\n    const directory = dirname(resolvedPath);\n\n    const parentDirectory = resolve(directory, \"..\");\n    const parentPath = resolve(parentDirectory, fileName);\n\n    if(parentDirectory === directory || directory === cwd){\n      continue;\n    }\n\n    resolvedPaths.push(parentPath);\n  }\n}\n\nexport function getModifiedDate(filePath: string): Date {\n  try {\n    const stats = statSync(filePath);\n    return stats.mtime;\n  } catch {\n    return new Date();\n  }\n}\n"
  },
  {
    "path": "src/async-utils/module.ts",
    "content": "export function isCommonJSModule() {\n  return typeof module !== \"undefined\" && typeof module.exports !== \"undefined\";\n}\n\nexport function isESModule() {\n  return !isCommonJSModule();\n}\n"
  },
  {
    "path": "src/async-utils/operations.ts",
    "content": "import type { GetCanonicalClasses } from \"../tailwindcss/canonical-classes.js\";\nimport type { GetClassOrder } from \"../tailwindcss/class-order.js\";\nimport type { GetConflictingClasses } from \"../tailwindcss/conflicting-classes.js\";\nimport type { GetCustomComponentClasses } from \"../tailwindcss/custom-component-classes.js\";\nimport type { GetDissectedClasses } from \"../tailwindcss/dissect-classes.js\";\nimport type { GetPrefix } from \"../tailwindcss/prefix.js\";\nimport type { GetUnknownClasses } from \"../tailwindcss/unknown-classes.js\";\nimport type { GetVariantOrder } from \"../tailwindcss/variant-order.js\";\nimport type { Async } from \"../types/async.js\";\n\n\nexport interface Operations {\n  getCanonicalClasses: Async<GetCanonicalClasses>;\n  getClassOrder: Async<GetClassOrder>;\n  getConflictingClasses: Async<GetConflictingClasses>;\n  getCustomComponentClasses: Async<GetCustomComponentClasses>;\n  getDissectedClasses: Async<GetDissectedClasses>;\n  getPrefix: Async<GetPrefix>;\n  getUnknownClasses: Async<GetUnknownClasses>;\n  getVariantOrder: Async<GetVariantOrder>;\n}\n\n// mapped type variant that enables correlated generic dispatch https://github.com/microsoft/TypeScript/issues/30581\nexport type OperationHandlers = {\n  [Operation in keyof Operations]: (...args: Parameters<Operations[Operation]>) => ReturnType<Operations[Operation]>;\n};\n"
  },
  {
    "path": "src/async-utils/order.ts",
    "content": "export enum VARIANT_ORDER_FLAGS {\n  GLOBAL = 1 << 16\n}\n"
  },
  {
    "path": "src/async-utils/path.ts",
    "content": "import { pathToFileURL } from \"node:url\";\n\nimport { isESModule } from \"./module.js\";\nimport { isWindows } from \"./platform.js\";\n\n\nexport function normalize(path: string): string {\n  return isWindows() && isESModule() ? pathToFileURL(path).toString() : path;\n}\n"
  },
  {
    "path": "src/async-utils/platform.ts",
    "content": "export function isWindows() {\n  return process.platform === \"win32\";\n}\n"
  },
  {
    "path": "src/async-utils/regex.ts",
    "content": "const REGEX_CACHE = new Map<string, RegExp>();\nconst MAX_CACHE_SIZE = 500;\n\n\nfunction getRegexCacheKey(pattern: string, flags: string): string {\n  return `${flags}\\u0000${pattern}`;\n}\n\nexport function getCachedRegex(regex: RegExp): RegExp;\nexport function getCachedRegex(pattern: string, flags?: string): RegExp;\nexport function getCachedRegex(patternOrRegex: RegExp | string, flags?: string): RegExp {\n  const regexFlags = typeof patternOrRegex === \"string\"\n    ? flags ?? \"\"\n    : patternOrRegex.flags;\n\n  const regexPattern = typeof patternOrRegex === \"string\"\n    ? patternOrRegex\n    : patternOrRegex.source;\n\n  const cacheKey = getRegexCacheKey(regexPattern, regexFlags);\n\n  let regex = REGEX_CACHE.get(cacheKey);\n\n  if(!regex){\n    if(REGEX_CACHE.size >= MAX_CACHE_SIZE){\n      const firstKey = REGEX_CACHE.keys().next().value;\n      if(firstKey !== undefined){\n        REGEX_CACHE.delete(firstKey);\n      }\n    }\n\n    regex = typeof patternOrRegex === \"string\"\n      ? new RegExp(patternOrRegex, flags)\n      : patternOrRegex;\n\n    REGEX_CACHE.set(cacheKey, regex);\n  }\n\n  if(regex.global || regex.sticky){\n    regex.lastIndex = 0;\n  }\n\n  return regex;\n}\n"
  },
  {
    "path": "src/async-utils/resolvers.ts",
    "content": "import fs from \"node:fs\";\n\nimport enhancedResolve from \"enhanced-resolve\";\nimport { TsconfigPathsPlugin } from \"tsconfig-paths-webpack-plugin\";\n\nimport { withCache } from \"../async-utils/cache.js\";\n\nimport type { AsyncContext } from \"../utils/context.js\";\n\n\nconst fileSystem = new enhancedResolve.CachedInputFileSystem(fs, 30_000);\n\nconst getESMResolver = (ctx: AsyncContext | undefined) => withCache(\"esm-resolver\", ctx?.tsconfigPath, () => enhancedResolve.ResolverFactory.createResolver({\n  conditionNames: [\"node\", \"import\"],\n  extensions: [\".mjs\", \".js\"],\n  fileSystem,\n  mainFields: [\"module\"],\n  plugins: ctx?.tsconfigPath ? [new TsconfigPathsPlugin({ configFile: ctx.tsconfigPath, mainFields: [\"module\"] })] : [],\n  useSyncFileSystemCalls: true\n}));\n\nconst getCJSResolver = (ctx: AsyncContext | undefined) => withCache(\"cjs-resolver\", ctx?.tsconfigPath, () => enhancedResolve.ResolverFactory.createResolver({\n  conditionNames: [\"node\", \"require\"],\n  extensions: [\".js\", \".cjs\"],\n  fileSystem,\n  mainFields: [\"main\"],\n  plugins: ctx?.tsconfigPath ? [new TsconfigPathsPlugin({ configFile: ctx.tsconfigPath, mainFields: [\"main\"] })] : [],\n  useSyncFileSystemCalls: true\n}));\n\nconst getCSSResolver = (ctx: AsyncContext | undefined) => withCache(\"css-resolver\", ctx?.tsconfigPath, () => enhancedResolve.ResolverFactory.createResolver({\n  conditionNames: [\"style\"],\n  extensions: [\".css\"],\n  fileSystem,\n  mainFields: [\"style\"],\n  plugins: ctx?.tsconfigPath ? [new TsconfigPathsPlugin({ configFile: ctx.tsconfigPath, mainFields: [\"style\"] })] : [],\n  useSyncFileSystemCalls: true\n}));\n\nconst jsonResolver = enhancedResolve.ResolverFactory.createResolver({\n  conditionNames: [\"json\"],\n  extensions: [\".json\"],\n  fileSystem,\n  useSyncFileSystemCalls: true\n});\n\n\nexport function resolveJs(path: string, cwd: string): string;\nexport function resolveJs(ctx: AsyncContext, path: string, cwd?: string): string;\nexport function resolveJs(ctxOrPath: AsyncContext | string | undefined, pathOrCwd: string, cwdOrUndefined?: string): string {\n  const ctx = typeof ctxOrPath === \"object\" ? ctxOrPath : undefined;\n  const path = typeof ctxOrPath === \"string\" ? ctxOrPath : pathOrCwd;\n  const cwd = (typeof ctxOrPath === \"object\" ? cwdOrUndefined : pathOrCwd)!;\n\n  try {\n    return getESMResolver(ctx).resolveSync({}, cwd, path) || path;\n  } catch {\n    return getCJSResolver(ctx).resolveSync({}, cwd, path) || path;\n  }\n}\n\nexport function resolveCss(path: string, cwd: string): string;\nexport function resolveCss(ctx: AsyncContext, path: string, cwd?: string): string;\nexport function resolveCss(ctxOrPath: AsyncContext | string | undefined, pathOrCwd: string, cwdOrUndefined?: string): string {\n  const ctx = typeof ctxOrPath === \"object\" ? ctxOrPath : undefined;\n  const path = typeof ctxOrPath === \"string\" ? ctxOrPath : pathOrCwd;\n  const cwd = (typeof ctxOrPath === \"object\" ? cwdOrUndefined : pathOrCwd)!;\n\n  try {\n    return getCSSResolver(ctx).resolveSync({}, cwd, path) || path;\n  } catch {\n    return path;\n  }\n}\n\nexport function resolveJson(path: string, cwd: string): string | undefined {\n  try {\n    return jsonResolver.resolveSync({}, cwd, path) || undefined;\n  } catch {}\n}\n"
  },
  {
    "path": "src/async-utils/tsconfig.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { withCache } from \"./cache.js\";\nimport { findPathRecursive } from \"./fs.js\";\n\nimport type { Warning } from \"../types/async.js\";\n\n\nexport interface GetTSConfigRequest {\n  configPath: string | undefined;\n  cwd: string;\n}\n\nexport interface GetTSConfigResponse {\n  path: string | undefined;\n  warnings: (Warning | undefined)[];\n}\n\nexport const getTSConfigPath = ({ configPath, cwd }: GetTSConfigRequest): GetTSConfigResponse => withCache(\"tsconfig-path\", configPath, () => {\n\n  const potentialPaths = [\n    ...configPath ? [configPath] : [],\n    \"tsconfig.json\",\n    \"jsconfig.json\"\n  ];\n\n  const foundConfigPath = findPathRecursive(cwd, cwd, potentialPaths);\n  const warning = getConfigPathWarning(configPath, foundConfigPath);\n\n  return {\n    path: foundConfigPath,\n    warnings: [warning]\n  };\n\n});\n\nfunction getConfigPathWarning(configPath: string | undefined, foundConfigPath: string | undefined): Warning | undefined {\n  if(!configPath){\n    return;\n  }\n\n  if(foundConfigPath && resolve(configPath) === resolve(foundConfigPath)){\n    return;\n  }\n\n  return {\n    option: \"tsconfig\",\n    title: `No tsconfig found at \\`${configPath}\\``\n  };\n}\n"
  },
  {
    "path": "src/async-utils/worker.ts",
    "content": "import { env } from \"node:process\";\n\nimport { TsRunner } from \"synckit\";\n\nimport type { SynckitOptions } from \"synckit\";\n\n\nconst defaultTimeout = 30_000;\n\nexport function getWorkerOptions(): SynckitOptions | undefined {\n  if(env.NODE_ENV === \"test\"){\n    return {\n      timeout: Number(env.SYNCKIT_TIMEOUT) || defaultTimeout,\n      tsRunner: TsRunner.OXC\n    };\n  } else {\n    return {\n      timeout: Number(env.SYNCKIT_TIMEOUT) || defaultTimeout\n    };\n  }\n}\n"
  },
  {
    "path": "src/configs/config.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport config from \"better-tailwindcss:configs/config.js\";\n\n\ndescribe(\"configs\", () => {\n  const stylisticRules = Object.entries(config.rules).reduce<string[]>((acc, [name, rule]) => {\n    if(rule.meta.docs.recommended && rule.meta.type === \"layout\"){\n      acc.push(`${config.meta.name}/${name}`);\n    }\n    return acc;\n  }, []);\n\n  const correctnessRules = Object.entries(config.rules).reduce<string[]>((acc, [name, rule]) => {\n    if(rule.meta.docs.recommended && rule.meta.type === \"problem\"){\n      acc.push(`${config.meta.name}/${name}`);\n    }\n    return acc;\n  }, []);\n\n  describe(\"stylistic\", () => {\n    it(\"should only contain recommended stylistic rules\", () => {\n      expect(Object.keys(config.configs.stylistic.rules)).toEqual(stylisticRules);\n    });\n  });\n\n  describe(\"correctness\", () => {\n    it(\"should only contain recommended correctness rules\", () => {\n      expect(Object.keys(config.configs.correctness.rules)).toEqual(correctnessRules);\n    });\n  });\n\n  describe(\"recommended\", () => {\n    it(\"should contain all recommended rules\", () => {\n      expect(Object.keys(config.configs.recommended.rules)).toEqual([\n        ...stylisticRules,\n        ...correctnessRules\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "src/configs/config.ts",
    "content": "import { enforceCanonicalClasses } from \"better-tailwindcss:rules/enforce-canonical-classes.js\";\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { enforceConsistentImportantPosition } from \"better-tailwindcss:rules/enforce-consistent-important-position.js\";\nimport { enforceConsistentLineWrapping } from \"better-tailwindcss:rules/enforce-consistent-line-wrapping.js\";\nimport { enforceConsistentVariableSyntax } from \"better-tailwindcss:rules/enforce-consistent-variable-syntax.js\";\nimport { enforceConsistentVariantOrder } from \"better-tailwindcss:rules/enforce-consistent-variant-order.js\";\nimport { enforceLogicalProperties } from \"better-tailwindcss:rules/enforce-logical-properties.js\";\nimport { enforceShorthandClasses } from \"better-tailwindcss:rules/enforce-shorthand-classes.js\";\nimport { noConflictingClasses } from \"better-tailwindcss:rules/no-conflicting-classes.js\";\nimport { noDeprecatedClasses } from \"better-tailwindcss:rules/no-deprecated-classes.js\";\nimport { noDuplicateClasses } from \"better-tailwindcss:rules/no-duplicate-classes.js\";\nimport { noRestrictedClasses } from \"better-tailwindcss:rules/no-restricted-classes.js\";\nimport { noUnknownClasses } from \"better-tailwindcss:rules/no-unknown-classes.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\n\nimport type { ESLint, Linter } from \"eslint\";\n\nimport type { RuleCategory } from \"better-tailwindcss:types/rule.js\";\n\n\ntype ConfigName = \"recommended\" | RuleCategory;\ntype Severity = \"error\" | \"warn\";\n\ntype PluginRules = typeof rules[number];\n\ntype PluginName = typeof plugin.meta.name;\n\ntype RuleObject = {\n  [Rule in PluginRules as Rule extends any ? Rule[\"name\"] : never]: Rule[\"rule\"]\n};\n\ntype GetRules<Category extends RuleCategory> = Extract<\n  PluginRules,\n  { category: Category; recommended: true; }\n>;\n\nconst rules = [\n  enforceConsistentClassOrder,\n  enforceConsistentImportantPosition,\n  enforceConsistentLineWrapping,\n  enforceConsistentVariantOrder,\n  enforceConsistentVariableSyntax,\n  enforceLogicalProperties,\n  enforceShorthandClasses,\n  noConflictingClasses,\n  noDeprecatedClasses,\n  noDuplicateClasses,\n  noRestrictedClasses,\n  noUnnecessaryWhitespace,\n  noUnknownClasses,\n  enforceCanonicalClasses\n] as const;\n\nconst plugin = {\n  meta: {\n    name: \"better-tailwindcss\"\n  },\n  rules: rules.reduce(\n    (acc, { name, rule }) => {\n      acc[name] = rule;\n      return acc;\n    },\n    {}\n  ) as RuleObject\n} as const satisfies ESLint.Plugin;\n\nconst getStylisticRules = <SeverityLevel extends Severity = \"warn\">(severity: SeverityLevel = \"warn\" as SeverityLevel) => {\n  return rules.reduce((acc, { category, name, recommended }) => {\n    if(category !== \"stylistic\" || !recommended){\n      return acc;\n    }\n\n    acc[`${plugin.meta.name}/${name}`] = severity;\n    return acc;\n  }, {} as Record<`${PluginName}/${GetRules<\"stylistic\">[\"name\"]}`, SeverityLevel>);\n};\n\nconst getCorrectnessRules = <SeverityLevel extends Severity = \"error\">(severity: SeverityLevel = \"error\" as SeverityLevel) => {\n  return rules.reduce((acc, { category, name, recommended }) => {\n    if(category !== \"correctness\" || !recommended){\n      return acc;\n    }\n\n    acc[`${plugin.meta.name}/${name}`] = severity;\n    return acc;\n  }, {} as Record<`${PluginName}/${GetRules<\"correctness\">[\"name\"]}`, SeverityLevel>);\n};\n\nconst getRecommendedRules = <SeverityLevel extends Severity>(severity?: SeverityLevel) => ({\n  ...getStylisticRules(severity),\n  ...getCorrectnessRules(severity)\n});\n\nconst createLegacyConfig = <Rules extends Linter.RulesRecord>(rules: Rules) => ({\n  plugins: [plugin.meta.name],\n  rules\n} satisfies Linter.LegacyConfig<Rules>);\n\nconst createFlatConfig = <Rules extends Linter.RulesRecord>(rules: Rules) => ({\n  plugins: {\n    [plugin.meta.name]: plugin\n  },\n  rules\n} satisfies Linter.Config<Rules>);\n\nconst configEntry = <ConfigName extends string, Config>(key: ConfigName, value: Config) => {\n  return { [key]: value } as { [Name in ConfigName]: Config };\n};\n\nconst createConfig = <Name extends ConfigName, RuleName extends string>(name: Name, getRules: <SeverityLevel extends Severity>(severity?: SeverityLevel) => Record<RuleName, SeverityLevel>) => {\n  return {\n    ...configEntry(`legacy-${name}` as const, createLegacyConfig(getRules())),\n    ...configEntry(\n      `legacy-${name}-error` as const,\n      createLegacyConfig(getRules(\"error\"))\n    ),\n    ...configEntry(\n      `legacy-${name}-warn` as const,\n      createLegacyConfig(getRules(\"warn\"))\n    ),\n    ...configEntry(name, createFlatConfig(getRules())),\n    ...configEntry(\n      `${name}-error` as const,\n      createFlatConfig(getRules(\"error\"))\n    ),\n    ...configEntry(`${name}-warn` as const, createFlatConfig(getRules(\"warn\")))\n  };\n};\n\nconst config = {\n  ...plugin,\n\n  configs: {\n    ...createConfig(\"stylistic\", getStylisticRules),\n    ...createConfig(\"correctness\", getCorrectnessRules),\n    ...createConfig(\"recommended\", getRecommendedRules)\n  }\n} satisfies ESLint.Plugin;\n\nexport default config;\n\nexport { config as \"module.exports\" };\n"
  },
  {
    "path": "src/options/callees/cc.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { CC_OBJECT_KEYS, CC_STRINGS } from \"better-tailwindcss:options/callees/cc.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"cc\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `cc(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `cc(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [CC_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      cc(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      cc(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [CC_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/cc.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CC_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^cc$\"\n} satisfies CalleeSelector;\n\nexport const CC_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^cc$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/jorgebucaran/classcat */\nexport const CC = [\n  CC_STRINGS,\n  CC_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/clb.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport {\n  CLB_BASE_VALUES,\n  CLB_COMPOUND_VARIANTS_CLASSES,\n  CLB_VARIANT_VALUES\n} from \"better-tailwindcss:options/callees/clb.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"clb\", () => {\n\n  it(\"should lint object values inside the `base` property\", () => {\n\n    const dirty = `\n      clb({\n          base: \" lint \",\n          \" ignore \": \" ignore \"\n        }\n      )\n    `;\n    const clean = `\n      clb({\n          base: \"lint\",\n          \" ignore \": \" ignore \"\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [CLB_BASE_VALUES] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object values inside the `variants` property\", () => {\n\n    const dirty = `\n      clb({\n          variants: { \" ignore \": \" lint \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n    const clean = `\n      clb({\n          variants: { \" ignore \": \"lint\" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [CLB_VARIANT_VALUES] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint only object values inside the `compoundVariants.classes` property\", () => {\n\n    const dirty = `\n      clb({\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"classes\": \" lint \"\n          }]\n        }\n      )\n    `;\n    const clean = `\n      clb({\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"classes\": \"lint\"\n          }]\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [CLB_COMPOUND_VARIANTS_CLASSES] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint all `clb` variations in combination by default\", () => {\n    const dirty = `\n      clb({\n        base: \" lint \",\n        compoundVariants: [\n          {\n            \" ignore \": \" ignore \",\n            \"classes\": \" lint \"\n          }\n        ],\n        defaultVariants: {\n          \" ignore \": \" ignore \"\n        },\n        variants: {\n          \" ignore \": {\n            \" ignore array \": [\n              \" lint \",\n              \" lint \"\n            ],\n            \" ignore string \": \" lint \"\n          }\n        }\n      });\n    `;\n\n    const clean = `\n      clb({\n        base: \"lint\",\n        compoundVariants: [\n          {\n            \" ignore \": \" ignore \",\n            \"classes\": \"lint\"\n          }\n        ],\n        defaultVariants: {\n          \" ignore \": \" ignore \"\n        },\n        variants: {\n          \" ignore \": {\n            \" ignore array \": [\n              \"lint\",\n              \"lint\"\n            ],\n            \" ignore string \": \"lint\"\n          }\n        }\n      });\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 10\n        }\n      ]\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/clb.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CLB_BASE_VALUES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^base$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^clb$\"\n} satisfies CalleeSelector;\n\nexport const CLB_VARIANT_VALUES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^variants.*$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^clb$\"\n} satisfies CalleeSelector;\n\nexport const CLB_COMPOUND_VARIANTS_CLASSES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.classes$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^clb$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/crswll/clb */\nexport const CLB = [\n  CLB_BASE_VALUES,\n  CLB_VARIANT_VALUES,\n  CLB_COMPOUND_VARIANTS_CLASSES\n  // TODO: add object key matcher: classes\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/clsx.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { CLSX_OBJECT_KEYS, CLSX_STRINGS } from \"better-tailwindcss:options/callees/clsx.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"clsx\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `clsx(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `clsx(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [CLSX_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      clsx(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      clsx(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [CLSX_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/clsx.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CLSX_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^clsx$\"\n} satisfies CalleeSelector;\n\nexport const CLSX_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^clsx$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/lukeed/clsx */\nexport const CLSX = [\n  CLSX_STRINGS,\n  CLSX_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/cn.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { CN_OBJECT_KEYS, CN_STRINGS } from \"better-tailwindcss:options/callees/cn.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"cn\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `cn(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `cn(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [CN_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      cn(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      cn(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [CN_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/cn.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CN_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^cn$\"\n} satisfies CalleeSelector;\n\nexport const CN_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^cn$\"\n} satisfies CalleeSelector;\n\n/** @see https://ui.shadcn.com/docs/installation/manual */\nexport const CN = [\n  CN_STRINGS,\n  CN_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/cnb.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { CNB_OBJECT_KEYS, CNB_STRINGS } from \"better-tailwindcss:options/callees/cnb.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"cnb\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `cnb(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `cnb(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [CNB_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      cnb(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      cnb(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [CNB_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/cnb.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CNB_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^cnb$\"\n} satisfies CalleeSelector;\n\nexport const CNB_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^cnb$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/xobotyi/cnbuilder */\nexport const CNB = [\n  CNB_STRINGS,\n  CNB_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/ctl.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { CTL_STRINGS } from \"better-tailwindcss:options/callees/ctl.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"ctl\", () => {\n\n  it(\"should lint strings\", () => {\n\n    const dirty = `ctl(\" lint \")`;\n    const clean = `ctl(\"lint\")`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [CTL_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/ctl.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CTL_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^ctl$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/netlify/classnames-template-literals */\nexport const CTL = [\n  CTL_STRINGS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/cva.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport {\n  CVA_COMPOUND_VARIANTS_CLASS,\n  CVA_STRINGS,\n  CVA_VARIANT_VALUES\n} from \"better-tailwindcss:options/callees/cva.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"cva\", () => {\n\n  it(\"should lint strings in arrays\", () => {\n\n    const dirty = `cva(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `cva(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [CVA_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object values inside the `variants` property\", () => {\n\n    const dirty = `\n      cva(\" ignore \", {\n          variants: { \" ignore \": \" lint \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n    const clean = `\n      cva(\" ignore \", {\n          variants: { \" ignore \": \"lint\" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [CVA_VARIANT_VALUES] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint only object values inside the `compoundVariants.class` and `compoundVariants.className` property\", () => {\n\n    const dirty = `\n      cva(\" ignore \", {\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"class\": \" lint \",\n            \"className\": \" lint \"\n          }]\n        }\n      )\n    `;\n    const clean = `\n      cva(\" ignore \", {\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"class\": \"lint\",\n            \"className\": \"lint\"\n          }]\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [CVA_COMPOUND_VARIANTS_CLASS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint all `cva` variations in combination by default\", () => {\n    const dirty = `\n      cva([\" lint \", \" lint \"], \" lint \", {\n        compoundVariants: [\n          {\n            \" ignore \": \" ignore \",\n            \"class\": \" lint \"\n          }\n        ],\n        defaultVariants: {\n          \" ignore \": \" ignore \"\n        },\n        variants: {\n          \" ignore \": {\n            \" ignore array \": [\n              \" lint \",\n              \" lint \"\n            ],\n            \" ignore string \": \" lint \"\n          }\n        }\n      });\n    `;\n\n    const clean = `\n      cva([\"lint\", \"lint\"], \"lint\", {\n        compoundVariants: [\n          {\n            \" ignore \": \" ignore \",\n            \"class\": \"lint\"\n          }\n        ],\n        defaultVariants: {\n          \" ignore \": \" ignore \"\n        },\n        variants: {\n          \" ignore \": {\n            \" ignore array \": [\n              \"lint\",\n              \"lint\"\n            ],\n            \" ignore string \": \"lint\"\n          }\n        }\n      });\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 14\n        }\n      ]\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/cva.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CVA_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^cva$\"\n} satisfies CalleeSelector;\n\nexport const CVA_VARIANT_VALUES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^variants.*$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^cva$\"\n} satisfies CalleeSelector;\n\nexport const CVA_COMPOUND_VARIANTS_CLASS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^cva$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/joe-bell/cva */\nexport const CVA = [\n  CVA_STRINGS,\n  CVA_VARIANT_VALUES,\n  CVA_COMPOUND_VARIANTS_CLASS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/cx.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { CX_OBJECT_KEYS, CX_STRINGS } from \"better-tailwindcss:options/callees/cx.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"cx\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `cx(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `cx(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [CX_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      cx(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      cx(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [CX_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/cx.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const CX_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^cx$\"\n} satisfies CalleeSelector;\n\nexport const CX_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^cx$\"\n} satisfies CalleeSelector;\n\n/** @see https://cva.style/docs/api-reference#cx */\nexport const CX = [\n  CX_STRINGS,\n  CX_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/dcnb.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { DCNB_OBJECT_KEYS, DCNB_STRINGS } from \"better-tailwindcss:options/callees/dcnb.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"dcnb\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `dcnb(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `dcnb(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [DCNB_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      dcnb(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      dcnb(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [DCNB_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/dcnb.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const DCNB_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^dcnb$\"\n} satisfies CalleeSelector;\n\nexport const DCNB_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^dcnb$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/xobotyi/cnbuilder */\nexport const DCNB = [\n  DCNB_STRINGS,\n  DCNB_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/objstr.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { OBJSTR_OBJECT_KEYS } from \"better-tailwindcss:options/callees/objstr.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"objstr\", () => {\n\n  it(\"should lint object keys\", () => {\n\n    const dirty = `\n      objstr(\" ignore \", {\n          \" lint \": { \" lint \": \" ignore \" },\n        }\n      )\n    `;\n    const clean = `\n      objstr(\" ignore \", {\n          \"lint\": { \"lint\": \" ignore \" },\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [OBJSTR_OBJECT_KEYS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/objstr.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const OBJSTR_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^objstr$\"\n} satisfies CalleeSelector;\n\nexport const OBJSTR_OBJECT_KEYS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.ObjectKey\n    }\n  ],\n  name: \"^objstr$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/lukeed/obj-str */\nexport const OBJSTR = [\n  OBJSTR_OBJECT_KEYS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/tv.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport {\n  TV_BASE_VALUES,\n  TV_COMPOUND_SLOTS_CLASS,\n  TV_COMPOUND_VARIANTS_CLASS,\n  TV_SLOTS_VALUES,\n  TV_STRINGS,\n  TV_VARIANT_VALUES\n} from \"better-tailwindcss:options/callees/tv.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"tv\", () => {\n\n  it(\"should lint strings in arrays\", () => {\n\n    const dirty = `tv(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `tv(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [TV_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object values inside the `variants` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          variants: { \" ignore \": \" lint \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          variants: { \" ignore \": \"lint\" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [TV_VARIANT_VALUES] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint only object values inside the `compoundVariants.class` and `compoundVariants.className` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"class\": \" lint \",\n            \"className\": \" lint \"\n          }]\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"class\": \"lint\",\n            \"className\": \"lint\"\n          }]\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TV_COMPOUND_VARIANTS_CLASS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint all `tv` variations in combination by default\", () => {\n    const dirty = `\n      tv([\" lint \", \" lint \"], \" lint \", {\n        base: \" lint \",\n        slots: {\n          \" ignore \": \" lint \"\n        },\n        compoundVariants: [\n          {\n            \" ignore \": \" ignore \",\n            \"class\": \" lint \"\n          }\n        ],\n        compoundSlots: [\n          {\n            slots: [\" ignore \", \" ignore \"],\n            \" ignore \": \" ignore \",\n            \"class\": \" lint \"\n          }\n        ],\n        defaultVariants: {\n          \" ignore \": \" ignore \"\n        },\n        variants: {\n          \" ignore \": {\n            \" ignore array \": [\n              \" lint \",\n              \" lint \"\n            ],\n            \" ignore string \": \" lint \"\n          }\n        }\n      });\n    `;\n\n    const clean = `\n      tv([\"lint\", \"lint\"], \"lint\", {\n        base: \"lint\",\n        slots: {\n          \" ignore \": \"lint\"\n        },\n        compoundVariants: [\n          {\n            \" ignore \": \" ignore \",\n            \"class\": \"lint\"\n          }\n        ],\n        compoundSlots: [\n          {\n            slots: [\" ignore \", \" ignore \"],\n            \" ignore \": \" ignore \",\n            \"class\": \"lint\"\n          }\n        ],\n        defaultVariants: {\n          \" ignore \": \" ignore \"\n        },\n        variants: {\n          \" ignore \": {\n            \" ignore array \": [\n              \"lint\",\n              \"lint\"\n            ],\n            \" ignore string \": \"lint\"\n          }\n        }\n      });\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 20\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint object values inside the `base` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          base: \" lint \",\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          base: \"lint\",\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 2,\n          options: [{ selectors: [TV_BASE_VALUES] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint object values inside the `slots` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          slots: { \n            slotName: \" lint \",\n            anotherSlot: [\" lint \", \" lint \"]\n          },\n          variants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          slots: { \n            slotName: \"lint\",\n            anotherSlot: [\"lint\", \"lint\"]\n          },\n          variants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [TV_SLOTS_VALUES] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint only object values inside the `compoundSlots.class` and `compoundSlots.className` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          slots: { \" ignore \": \" ignore \" },\n          compoundSlots: [{ \n            slots: [\" ignore \", \" ignore \"],\n            \" ignore \": \" ignore \",\n            \"class\": \" lint \",\n            \"className\": \" lint \"\n          }]\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          slots: { \" ignore \": \" ignore \" },\n          compoundSlots: [{ \n            slots: [\" ignore \", \" ignore \"],\n            \" ignore \": \" ignore \",\n            \"class\": \"lint\",\n            \"className\": \"lint\"\n          }]\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TV_COMPOUND_SLOTS_CLASS] }]\n        }\n      ]\n    });\n\n  });\n\n\n  it(\"should lint string arrays inside the `compoundVariants.class` and `compoundVariants.className` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          slots: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"class\": [\" lint \", \" lint \"],\n            \"className\": [\" lint \", \" lint \", \" lint \"]\n          }]\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          slots: { \" ignore \": \" ignore \" },\n          compoundVariants: [{ \n            \" ignore \": \" ignore \",\n            \"class\": [\"lint\", \"lint\"],\n            \"className\": [\"lint\", \"lint\", \"lint\"]\n          }]\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 10,\n          options: [{ selectors: [TV_COMPOUND_VARIANTS_CLASS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint string arrays inside the `compoundSlots.class` and `compoundSlots.className` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          slots: { \" ignore \": \" ignore \" },\n          compoundSlots: [{ \n            slots: [\" ignore \", \" ignore \"],\n            \" ignore \": \" ignore \",\n            \"class\": [\" lint \", \" lint \"],\n            \"className\": [\" lint \", \" lint \", \" lint \"]\n          }]\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          slots: { \" ignore \": \" ignore \" },\n          compoundSlots: [{ \n            slots: [\" ignore \", \" ignore \"],\n            \" ignore \": \" ignore \",\n            \"class\": [\"lint\", \"lint\"],\n            \"className\": [\"lint\", \"lint\", \"lint\"]\n          }]\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 10,\n          options: [{ selectors: [TV_COMPOUND_SLOTS_CLASS] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should lint string arrays inside the `base` property\", () => {\n\n    const dirty = `\n      tv(\" ignore \", {\n          base: [\" lint \", \" lint \", \" lint \"],\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n    const clean = `\n      tv(\" ignore \", {\n          base: [\"lint\", \"lint\", \"lint\"],\n          variants: { \" ignore \": \" ignore \" },\n          compoundVariants: { \" ignore \": \" ignore \" }\n        }\n      )\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [TV_BASE_VALUES] }]\n        }\n      ]\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/tv.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const TV_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^tv$\"\n} satisfies CalleeSelector;\n\nexport const TV_VARIANT_VALUES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^variants.*$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^tv$\"\n} satisfies CalleeSelector;\n\nexport const TV_BASE_VALUES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^base$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^tv$\"\n} satisfies CalleeSelector;\n\nexport const TV_SLOTS_VALUES = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^slots.*$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^tv$\"\n} satisfies CalleeSelector;\n\nexport const TV_COMPOUND_VARIANTS_CLASS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class).*$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^tv$\"\n} satisfies CalleeSelector;\n\nexport const TV_COMPOUND_SLOTS_CLASS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      path: \"^compoundSlots\\\\[\\\\d+\\\\]\\\\.(?:className|class).*$\",\n      type: MatcherType.ObjectValue\n    }\n  ],\n  name: \"^tv$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/nextui-org/tailwind-variants?tab=readme-ov-file */\nexport const TV = [\n  TV_STRINGS,\n  TV_VARIANT_VALUES,\n  TV_COMPOUND_VARIANTS_CLASS,\n  TV_BASE_VALUES,\n  TV_SLOTS_VALUES,\n  TV_COMPOUND_SLOTS_CLASS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/twJoin.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { TW_JOIN_STRINGS } from \"better-tailwindcss:options/callees/twJoin.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"twJoin\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `twJoin(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `twJoin(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [TW_JOIN_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/twJoin.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const TW_JOIN_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^twJoin$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/dcastil/tailwind-merge */\nexport const TW_JOIN = [\n  TW_JOIN_STRINGS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/callees/twMerge.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { TW_MERGE_STRINGS } from \"better-tailwindcss:options/callees/twMerge.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"twMerge\", () => {\n\n  it(\"should lint strings and strings in arrays\", () => {\n\n    const dirty = `twMerge(\" lint \", [\" lint \", \" lint \"])`;\n    const clean = `twMerge(\"lint\", [\"lint\", \"lint\"])`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirty,\n          jsxOutput: clean,\n          svelte: `<script>${dirty}</script>`,\n          svelteOutput: `<script>${clean}</script>`,\n          vue: `<script>${dirty}</script>`,\n          vueOutput: `<script>${clean}</script>`,\n\n          errors: 6,\n          options: [{ selectors: [TW_MERGE_STRINGS] }]\n        }\n      ]\n    });\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/callees/twMerge.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const TW_MERGE_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    }\n  ],\n  name: \"^twMerge$\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/dcastil/tailwind-merge */\nexport const TW_MERGE = [\n  TW_MERGE_STRINGS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/default-options.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { DEFAULT_CALLEE_SELECTORS, DEFAULT_TAG_SELECTORS } from \"better-tailwindcss:options/default-options.js\";\nimport { getFilesInDirectory } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"default options\", () => {\n\n  it(\"should include all callees by default\", () => {\n    const callees = DEFAULT_CALLEE_SELECTORS\n      .filter(selector => selector.kind === SelectorKind.Callee)\n      .map(selector => selector.name)\n      .filter((callee, index, arr) => arr.indexOf(callee) === index);\n\n    const exportedFiles = getFilesInDirectory(\"./src/options/callees/\");\n    const fileNames = exportedFiles.map(file => file.replace(\".ts\", \"\"));\n\n    expect(callees.sort()).toStrictEqual(fileNames.sort().map(name => `^${name}$`));\n\n  });\n\n  it(\"should include all tags by default\", () => {\n    const tags = DEFAULT_TAG_SELECTORS\n      .filter(selector => selector.kind === SelectorKind.Tag)\n      .map(selector => selector.path)\n      .filter((tag, index, arr) => arr.indexOf(tag) === index);\n\n    const exportedFiles = getFilesInDirectory(\"./src/options/tags/\");\n    const fileNames = exportedFiles.map(file => file.replace(\".ts\", \"\"));\n\n    expect(tags.sort()).toStrictEqual(fileNames.sort().map(name => `${name}(\\\\.\\\\w+)?`));\n  });\n\n});\n"
  },
  {
    "path": "src/options/default-options.ts",
    "content": "import { CC } from \"better-tailwindcss:options/callees/cc.js\";\nimport { CLB } from \"better-tailwindcss:options/callees/clb.js\";\nimport { CLSX } from \"better-tailwindcss:options/callees/clsx.js\";\nimport { CN } from \"better-tailwindcss:options/callees/cn.js\";\nimport { CNB } from \"better-tailwindcss:options/callees/cnb.js\";\nimport { CTL } from \"better-tailwindcss:options/callees/ctl.js\";\nimport { CVA } from \"better-tailwindcss:options/callees/cva.js\";\nimport { CX } from \"better-tailwindcss:options/callees/cx.js\";\nimport { DCNB } from \"better-tailwindcss:options/callees/dcnb.js\";\nimport { OBJSTR } from \"better-tailwindcss:options/callees/objstr.js\";\nimport { TV } from \"better-tailwindcss:options/callees/tv.js\";\nimport { TW_JOIN } from \"better-tailwindcss:options/callees/twJoin.js\";\nimport { TW_MERGE } from \"better-tailwindcss:options/callees/twMerge.js\";\nimport { TWC } from \"better-tailwindcss:options/tags/twc.js\";\nimport { TWX } from \"better-tailwindcss:options/tags/twx.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { Variables } from \"better-tailwindcss:options/schemas/variables.js\";\nimport type { Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const DEFAULT_CALLEE_SELECTORS = [\n  ...CC,\n  ...CLB,\n  ...CLSX,\n  ...CN,\n  ...CNB,\n  ...CTL,\n  ...CVA,\n  ...CX,\n  ...DCNB,\n  ...OBJSTR,\n  ...TV,\n  ...TW_JOIN,\n  ...TW_MERGE\n] satisfies Selectors;\n\nexport const DEFAULT_ATTRIBUTE_SELECTORS = [\n  {\n    kind: SelectorKind.Attribute,\n    name: \"^class(?:Name)?$\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.String\n      }\n    ],\n    name: \"^class(?:Name)?$\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.String\n      }\n    ],\n    name: \"^class:.*$\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    name: \"(?:^\\\\[class\\\\]$)|(?:^\\\\[ngClass\\\\]$)\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.String\n      }\n    ],\n    name: \"(?:^\\\\[class\\\\..*\\\\]$)\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.String\n      },\n      {\n        type: MatcherType.ObjectKey\n      }\n    ],\n    name: \"(?:^\\\\[class\\\\]$)|(?:^\\\\[ngClass\\\\]$)\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.String\n      },\n      {\n        type: MatcherType.ObjectKey\n      }\n    ],\n    name: \"^v-bind:class$\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.String\n      },\n      {\n        type: MatcherType.ObjectKey\n      }\n    ],\n    name: \"^class:list$\"\n  },\n  {\n    kind: SelectorKind.Attribute,\n    match: [\n      {\n        type: MatcherType.ObjectKey\n      }\n    ],\n    name: \"^classList$\"\n  }\n] satisfies Selectors;\n\nexport const DEFAULT_VARIABLE_NAMES = [\n  [\n    \"^classNames?$\", [\n      {\n        match: MatcherType.String\n      }\n    ]\n  ],\n  [\n    \"^classes$\", [\n      {\n        match: MatcherType.String\n      }\n    ]\n  ],\n  [\n    \"^styles?$\", [\n      {\n        match: MatcherType.String\n      }\n    ]\n  ]\n] satisfies Variables;\n\nexport const DEFAULT_VARIABLE_SELECTORS = [\n  {\n    kind: SelectorKind.Variable,\n    match: [\n      {\n        type: MatcherType.String\n      }\n    ],\n    name: \"^classNames?$\"\n  },\n  {\n    kind: SelectorKind.Variable,\n    match: [\n      {\n        type: MatcherType.String\n      }\n    ],\n    name: \"^classes$\"\n  },\n  {\n    kind: SelectorKind.Variable,\n    match: [\n      {\n        type: MatcherType.String\n      }\n    ],\n    name: \"^styles?$\"\n  }\n] satisfies Selectors;\n\nexport const DEFAULT_TAG_SELECTORS = [\n  ...TWC,\n  ...TWX\n] satisfies Selectors;\n\nexport const DEFAULT_SELECTORS = [\n  ...DEFAULT_ATTRIBUTE_SELECTORS,\n  ...DEFAULT_CALLEE_SELECTORS,\n  ...DEFAULT_VARIABLE_SELECTORS,\n  ...DEFAULT_TAG_SELECTORS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/descriptions.test.ts",
    "content": "import { toJsonSchema } from \"@valibot/to-json-schema\";\nimport { validate } from \"json-schema\";\nimport { describe, expect, test } from \"vitest\";\n\nimport { COMMON_OPTIONS } from \"better-tailwindcss:options/descriptions.js\";\nimport { ATTRIBUTES_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/attributes.js\";\nimport { CALLEES_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/callees.js\";\nimport { VARIABLES_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/variables.js\";\nimport { MatcherType } from \"better-tailwindcss:types/rule.js\";\n\nimport type { AttributesOptions } from \"better-tailwindcss:options/schemas/attributes.js\";\nimport type { CalleesOptions } from \"better-tailwindcss:options/schemas/callees.js\";\nimport type { VariablesOptions } from \"better-tailwindcss:options/schemas/variables.js\";\n\n\ndescribe(\"descriptions\", () => {\n\n  test(\"name config\", () => {\n\n    const attributes = {\n      attributes: [\n        \"class\",\n        \"className\"\n      ]\n    } satisfies AttributesOptions;\n\n    expect(\n      validate(attributes, toJsonSchema(ATTRIBUTES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n    const callees = {\n      callees: [\n        \"callee\"\n      ]\n    } satisfies CalleesOptions;\n\n    expect(\n      validate(callees, toJsonSchema(CALLEES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n    const variable = {\n      variables: [\n        \"classes\",\n        \"styles\"\n      ]\n    } satisfies VariablesOptions;\n\n    expect(\n      validate(variable, toJsonSchema(VARIABLES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n  });\n\n  test(\"regex config\", () => {\n\n    const attributes = {\n      attributes: [\n        \"(class|className)\",\n        \"(.*)\"\n      ]\n    } satisfies AttributesOptions;\n\n    expect(\n      validate(attributes, toJsonSchema(ATTRIBUTES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n    const callees = {\n      callees: [\n        \"callee(.*)\",\n        \"(.*)\"\n      ]\n    } satisfies CalleesOptions;\n\n    expect(\n      validate(callees, toJsonSchema(CALLEES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n    const variable = {\n      variables: [\n        \"variable = (.*)\",\n        \"(.*)\"\n      ]\n    } satisfies VariablesOptions;\n\n    expect(\n      validate(variable, toJsonSchema(VARIABLES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n  });\n\n  test(\"matcher config\", () => {\n\n    const attributes: AttributesOptions = {\n      attributes: [\n        [\n          \"class\",\n          [\n            {\n              match: MatcherType.String\n            },\n            {\n              match: MatcherType.ObjectKey,\n              pathPattern: \"^.*\"\n            },\n            {\n              match: MatcherType.ObjectValue\n            }\n          ]\n        ]\n      ]\n    };\n\n    expect(\n      validate(attributes, toJsonSchema(ATTRIBUTES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n    const callees: CalleesOptions = {\n      callees: [\n        [\n          \"callee\",\n          [\n            {\n              match: MatcherType.String\n            },\n            {\n              match: MatcherType.ObjectKey,\n              pathPattern: \"^.*\"\n            },\n            {\n              match: MatcherType.ObjectValue\n            }\n          ]\n        ]\n      ]\n    };\n\n    expect(\n      validate(callees, toJsonSchema(CALLEES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n    const variable: VariablesOptions = {\n      variables: [\n        [\n          \"variable\",\n          [\n            {\n              match: MatcherType.String\n            },\n            {\n              match: MatcherType.ObjectKey,\n              pathPattern: \"^.*\"\n            },\n            {\n              match: MatcherType.ObjectValue\n            }\n          ]\n        ]\n      ]\n    };\n\n    expect(\n      validate(variable, toJsonSchema(VARIABLES_OPTION_SCHEMA))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n  });\n\n  test(\"selectors config\", () => {\n\n    const selectors = {\n      selectors: [\n        {\n          callTarget: -1,\n          kind: \"callee\",\n          match: [\n            {\n              type: MatcherType.String\n            }\n          ],\n          path: \"^classes\\\\.push$\"\n        },\n        {\n          kind: \"tag\",\n          name: \"^tw$\"\n        },\n        {\n          kind: \"attribute\",\n          match: [\n            {\n              pathPattern: \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\",\n              type: MatcherType.ObjectValue\n            }\n          ],\n          name: \"^class(?:Name)?$\"\n        }\n      ]\n    };\n\n    expect(\n      validate(selectors, toJsonSchema(COMMON_OPTIONS))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n  });\n\n  test(\"selectors targetCall and targetArgument config\", () => {\n\n    const selectors = {\n      selectors: [\n        {\n          kind: \"callee\",\n          name: \"^cn$\",\n          targetArgument: \"first\",\n          targetCall: \"last\"\n        },\n        {\n          callTarget: -1,\n          kind: \"callee\",\n          name: \"^legacy$\",\n          targetArgument: 0\n        }\n      ]\n    };\n\n    expect(\n      validate(selectors, toJsonSchema(COMMON_OPTIONS))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n  });\n\n  test(\"selectors anonymousFunctionReturn matcher config\", () => {\n\n    const selectors = {\n      selectors: [\n        {\n          kind: \"callee\",\n          match: [{\n            match: [\n              { type: MatcherType.String },\n              { type: MatcherType.ObjectKey },\n              { path: \"^compoundVariants\\\\[\\\\d+\\\\]\\\\.(?:className|class)$\", type: MatcherType.ObjectValue }\n            ],\n            type: MatcherType.AnonymousFunctionReturn\n          }],\n          name: \"^classFactory$\"\n        }\n      ]\n    };\n\n    expect(\n      validate(selectors, toJsonSchema(COMMON_OPTIONS))\n    ).toStrictEqual(\n      { errors: [], valid: true }\n    );\n\n  });\n\n});\n"
  },
  {
    "path": "src/options/descriptions.ts",
    "content": "import { strictObject } from \"valibot\";\n\nimport { ATTRIBUTES_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/attributes.js\";\nimport { CALLEES_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/callees.js\";\nimport {\n  CWD_OPTION_SCHEMA,\n  DETECT_COMPONENT_CLASSES_OPTION_SCHEMA,\n  ENTRYPOINT_OPTION_SCHEMA,\n  MESSAGE_STYLE_OPTION_SCHEMA,\n  ROOT_FONT_SIZE_OPTION_SCHEMA,\n  TAILWIND_OPTION_SCHEMA,\n  TSCONFIG_OPTION_SCHEMA\n} from \"better-tailwindcss:options/schemas/common.js\";\nimport { SELECTORS_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/selectors.js\";\nimport { TAGS_OPTIONS_SCHEMA } from \"better-tailwindcss:options/schemas/tags.js\";\nimport { VARIABLES_OPTION_SCHEMA } from \"better-tailwindcss:options/schemas/variables.js\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nexport const COMMON_OPTIONS = strictObject({\n  ...SELECTORS_OPTION_SCHEMA.entries,\n  ...CALLEES_OPTION_SCHEMA.entries,\n  ...ATTRIBUTES_OPTION_SCHEMA.entries,\n  ...VARIABLES_OPTION_SCHEMA.entries,\n  ...TAGS_OPTIONS_SCHEMA.entries,\n  ...ENTRYPOINT_OPTION_SCHEMA.entries,\n  ...MESSAGE_STYLE_OPTION_SCHEMA.entries,\n  ...TAILWIND_OPTION_SCHEMA.entries,\n  ...TSCONFIG_OPTION_SCHEMA.entries,\n  ...DETECT_COMPONENT_CLASSES_OPTION_SCHEMA.entries,\n  ...ROOT_FONT_SIZE_OPTION_SCHEMA.entries,\n  ...CWD_OPTION_SCHEMA.entries\n});\n\nexport type CommonOptions = InferOutput<typeof COMMON_OPTIONS>;\n"
  },
  {
    "path": "src/options/migrate.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport {\n  hasLegacySelectorConfig,\n  migrateFlatSelectorsToLegacySelectors,\n  migrateLegacySelectorsToFlatSelectors\n} from \"better-tailwindcss:options/migrate.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"migrate\", () => {\n  test(\"should migrate legacy selectors to flat selectors\", () => {\n    const selectors = migrateLegacySelectorsToFlatSelectors({\n      attributes: [\"^class(?:Name)?$\"],\n      callees: [[\"^cva$\", [{ match: MatcherType.String }]]]\n    });\n\n    expect(selectors).toStrictEqual([\n      {\n        kind: SelectorKind.Attribute,\n        name: \"^class(?:Name)?$\"\n      },\n      {\n        kind: SelectorKind.Callee,\n        match: [{ type: MatcherType.String }],\n        name: \"^cva$\",\n        path: \"^cva$\"\n      }\n    ]);\n  });\n\n  test(\"should detect legacy selector config\", () => {\n    expect(hasLegacySelectorConfig({})).toBe(false);\n    expect(hasLegacySelectorConfig({ attributes: [] })).toBe(true);\n    expect(hasLegacySelectorConfig({ callees: [] })).toBe(true);\n    expect(hasLegacySelectorConfig({ tags: [] })).toBe(true);\n    expect(hasLegacySelectorConfig({ variables: [] })).toBe(true);\n  });\n\n  test(\"should migrate flat selectors to legacy selectors\", () => {\n    const selectors = migrateFlatSelectorsToLegacySelectors([\n      {\n        kind: SelectorKind.Attribute,\n        name: \"^class$\"\n      },\n      {\n        kind: SelectorKind.Callee,\n        match: [{ type: MatcherType.String }],\n        name: \"^cva$\"\n      },\n      {\n        kind: SelectorKind.Variable,\n        match: [{ path: \"^foo$\", type: MatcherType.ObjectKey }],\n        name: \"^classes$\"\n      }\n    ]);\n\n    expect(selectors).toStrictEqual({\n      attributes: [\"^class$\"],\n      callees: [[\"^cva$\", [{ match: MatcherType.String }]]],\n      variables: [[\"^classes$\", [{ match: MatcherType.ObjectKey, pathPattern: \"^foo$\" }]]]\n    });\n  });\n\n  test(\"should skip selectors with anonymousFunctionReturn matcher when migrating to legacy\", () => {\n    const selectors = migrateFlatSelectorsToLegacySelectors([\n      {\n        kind: SelectorKind.Callee,\n        match: [{ type: MatcherType.String }],\n        name: \"^cva$\"\n      },\n      {\n        kind: SelectorKind.Callee,\n        match: [{\n          match: [{ type: MatcherType.String }],\n          type: MatcherType.AnonymousFunctionReturn\n        }],\n        name: \"^classFactory$\"\n      }\n    ]);\n\n    expect(selectors).toStrictEqual({\n      callees: [[\"^cva$\", [{ match: MatcherType.String }]]]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/migrate.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { Attributes } from \"better-tailwindcss:options/schemas/attributes.js\";\nimport type { Callees } from \"better-tailwindcss:options/schemas/callees.js\";\nimport type { Tags } from \"better-tailwindcss:options/schemas/tags.js\";\nimport type { Variables } from \"better-tailwindcss:options/schemas/variables.js\";\nimport type { Matcher, Selector, SelectorMatcher, Selectors } from \"better-tailwindcss:types/rule.js\";\n\n\ntype LegacySelector = Attributes[number] | Callees[number] | Tags[number] | Variables[number];\n\ntype LegacySelectorsByKind = {\n  attributes?: Attributes | undefined;\n  callees?: Callees | undefined;\n  tags?: Tags | undefined;\n  variables?: Variables | undefined;\n};\n\nexport function migrateLegacySelectorsToFlatSelectors(legacy: LegacySelectorsByKind): Selectors {\n  const selectors: Selectors = [];\n\n  if(legacy.attributes){\n    for(const attributeSelector of legacy.attributes){\n      selectors.push(migrateLegacySelector(attributeSelector, SelectorKind.Attribute));\n    }\n  }\n  if(legacy.callees){\n    for(const calleeSelector of legacy.callees){\n      selectors.push(migrateLegacySelector(calleeSelector, SelectorKind.Callee));\n    }\n  }\n  if(legacy.tags){\n    for(const tagSelector of legacy.tags){\n      selectors.push(migrateLegacySelector(tagSelector, SelectorKind.Tag));\n    }\n  }\n  if(legacy.variables){\n    for(const variableSelector of legacy.variables){\n      selectors.push(migrateLegacySelector(variableSelector, SelectorKind.Variable));\n    }\n  }\n\n  return selectors;\n}\n\nexport function migrateFlatSelectorsToLegacySelectors(selectors: Selectors): LegacySelectorsByKind {\n  return selectors.reduce<LegacySelectorsByKind>((legacy, selector) => {\n    const migratedSelector = migrateFlatSelector(selector);\n\n    if(migratedSelector === undefined){\n      return legacy;\n    }\n\n    switch (selector.kind){\n      case SelectorKind.Attribute:\n        (legacy.attributes ??= []).push(migratedSelector);\n        break;\n      case SelectorKind.Callee:\n        (legacy.callees ??= []).push(migratedSelector);\n        break;\n      case SelectorKind.Tag:\n        (legacy.tags ??= []).push(migratedSelector);\n        break;\n      case SelectorKind.Variable:\n        (legacy.variables ??= []).push(migratedSelector);\n        break;\n    }\n\n    return legacy;\n  }, {});\n}\n\nexport function hasLegacySelectorConfig(options: LegacySelectorsByKind): boolean {\n  return (\n    options.attributes !== undefined ||\n    options.callees !== undefined ||\n    options.tags !== undefined ||\n    options.variables !== undefined\n  );\n}\n\nfunction toSelectorMatcher(matcher: Matcher): SelectorMatcher {\n  if(matcher.match === MatcherType.String){\n    return {\n      type: matcher.match\n    };\n  }\n\n  return {\n    ...matcher.pathPattern !== undefined && {\n      path: matcher.pathPattern\n    },\n    type: matcher.match\n  };\n}\n\nfunction toLegacyMatcher(matcher: SelectorMatcher): Matcher | undefined {\n  if(matcher.type === MatcherType.AnonymousFunctionReturn){\n    return;\n  }\n\n  if(matcher.type === MatcherType.String){\n    return {\n      match: matcher.type\n    };\n  }\n\n  return {\n    ...matcher.path !== undefined && {\n      pathPattern: matcher.path\n    },\n    match: matcher.type\n  };\n}\n\nfunction migrateLegacySelector(selector: LegacySelector, kind: SelectorKind) {\n  const name = typeof selector === \"string\" ? selector : selector[0];\n  const path = kind === SelectorKind.Callee || kind === SelectorKind.Tag ? name : undefined;\n  const matchers = typeof selector === \"string\" ? undefined : selector[1].map(toSelectorMatcher);\n\n  if(matchers === undefined){\n    return {\n      kind,\n      name,\n      ...path ? { path } : {}\n    };\n  }\n\n  return {\n    kind,\n    match: matchers,\n    name,\n    ...path ? { path } : {}\n  };\n}\n\nfunction migrateFlatSelector(selector: Selector): LegacySelector | undefined {\n  if(selector.kind === SelectorKind.Callee || selector.kind === SelectorKind.Tag){\n    if(selector.match === undefined){\n      return selector.name ?? selector.path!;\n    }\n\n    const legacyMatchers = selector.match\n      .map(toLegacyMatcher)\n      .filter((matcher): matcher is Matcher => matcher !== undefined);\n\n    if(legacyMatchers.length !== selector.match.length){\n      return;\n    }\n\n    return [\n      selector.name ?? selector.path!,\n      legacyMatchers\n    ];\n  }\n\n  if(selector.match === undefined){\n    return selector.name;\n  }\n\n  const legacyMatchers = selector.match\n    .map(toLegacyMatcher)\n    .filter((matcher): matcher is Matcher => matcher !== undefined);\n\n  if(legacyMatchers.length !== selector.match.length){\n    return;\n  }\n\n  return [\n    selector.name,\n    legacyMatchers\n  ];\n}\n"
  },
  {
    "path": "src/options/schemas/attributes.ts",
    "content": "import {\n  array,\n  description,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  tuple,\n  union\n} from \"valibot\";\n\nimport {\n  OBJECT_KEY_MATCHER_SCHEMA,\n  OBJECT_VALUE_MATCHER_SCHEMA,\n  STRING_MATCHER_SCHEMA\n} from \"better-tailwindcss:options/schemas/matchers.js\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nexport const ATTRIBUTE_MATCHER_CONFIG = pipe(\n  tuple([\n    pipe(\n      string(),\n      description(\"Attribute name for which children get linted if matched.\")\n    ),\n    pipe(\n      array(\n        union([\n          STRING_MATCHER_SCHEMA,\n          OBJECT_KEY_MATCHER_SCHEMA,\n          OBJECT_VALUE_MATCHER_SCHEMA\n        ])\n      ),\n      description(\"List of matchers that will be applied.\")\n    )\n  ]),\n  description(\"List of matchers that will automatically be matched.\")\n);\n\nexport type AttributeMatchers = InferOutput<typeof ATTRIBUTE_MATCHER_CONFIG>;\n\nexport const ATTRIBUTE_NAME_CONFIG = pipe(\n  string(),\n  description(\"Attribute name that for which children get linted.\")\n);\n\nexport type AttributeName = InferOutput<typeof ATTRIBUTE_NAME_CONFIG>;\n\nexport const ATTRIBUTES_SCHEMA = pipe(\n  array(\n    union([\n      ATTRIBUTE_NAME_CONFIG,\n      ATTRIBUTE_MATCHER_CONFIG\n    ])\n  ),\n  description(\"List of attribute names that should get linted.\")\n);\n\nexport type Attributes = InferOutput<typeof ATTRIBUTES_SCHEMA>;\n\nexport const ATTRIBUTES_OPTION_SCHEMA = strictObject({\n  attributes: optional(ATTRIBUTES_SCHEMA)\n});\n\nexport type AttributesOptions = InferOutput<typeof ATTRIBUTES_OPTION_SCHEMA>;\n"
  },
  {
    "path": "src/options/schemas/callees.ts",
    "content": "import {\n  array,\n  description,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  tuple,\n  union\n} from \"valibot\";\n\nimport {\n  OBJECT_KEY_MATCHER_SCHEMA,\n  OBJECT_VALUE_MATCHER_SCHEMA,\n  STRING_MATCHER_SCHEMA\n} from \"better-tailwindcss:options/schemas/matchers.js\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nconst CALLEE_MATCHER_SCHEMA = pipe(\n  tuple([\n    pipe(\n      string(),\n      description(\"Callee name for which children get linted if matched.\")\n    ),\n    pipe(\n      array(\n        union([\n          STRING_MATCHER_SCHEMA,\n          OBJECT_KEY_MATCHER_SCHEMA,\n          OBJECT_VALUE_MATCHER_SCHEMA\n        ])\n      ),\n      description(\"List of matchers that will be applied.\")\n    )\n  ]),\n  description(\"List of matchers that will automatically be matched.\")\n);\n\nexport type CalleeMatchers = InferOutput<typeof CALLEE_MATCHER_SCHEMA>;\n\nconst CALLEE_NAME_SCHEMA = pipe(\n  string(),\n  description(\"Callee name for which children get linted.\")\n);\n\nexport type CalleeName = InferOutput<typeof CALLEE_NAME_SCHEMA>;\n\nexport const CALLEES_SCHEMA = pipe(\n  array(\n    union([\n      CALLEE_MATCHER_SCHEMA,\n      CALLEE_NAME_SCHEMA\n    ])\n  ),\n  description(\"List of function names which arguments should get linted.\")\n);\n\nexport type Callees = InferOutput<typeof CALLEES_SCHEMA>;\n\nexport const CALLEES_OPTION_SCHEMA = strictObject({\n  callees: optional(CALLEES_SCHEMA)\n});\n\nexport type CalleesOptions = InferOutput<typeof CALLEES_OPTION_SCHEMA>;\n"
  },
  {
    "path": "src/options/schemas/common.ts",
    "content": "import { env } from \"node:process\";\n\nimport {\n  boolean,\n  description,\n  literal,\n  number,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  union\n} from \"valibot\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nexport const ENTRYPOINT_OPTION_SCHEMA = strictObject({\n  entryPoint: optional(\n    pipe(\n      string(),\n      description(\"The path to the css entry point of the project. If not specified, the plugin will fall back to the default tailwind classes.\")\n    ),\n    undefined\n  )\n});\n\nexport type EntryPointOption = InferOutput<typeof ENTRYPOINT_OPTION_SCHEMA>;\n\nexport const TAILWIND_OPTION_SCHEMA = strictObject({\n  tailwindConfig: optional(\n    pipe(\n      string(),\n      description(\"The path to the tailwind config file. If not specified, the plugin will try to find it automatically or falls back to the default configuration.\")\n    ),\n    undefined\n  )\n});\n\nexport type TailwindConfigOption = InferOutput<typeof TAILWIND_OPTION_SCHEMA>;\n\nexport const TSCONFIG_OPTION_SCHEMA = strictObject({\n  tsconfig: optional(\n    pipe(\n      string(),\n      description(\"The path to the tsconfig file. Is used to resolve path aliases in the tsconfig.\")\n    ),\n    undefined\n  )\n});\n\nexport type TSConfigOption = InferOutput<typeof TSCONFIG_OPTION_SCHEMA>;\n\nexport const MESSAGE_STYLE_OPTION_SCHEMA = strictObject({\n  messageStyle: optional(\n    pipe(\n      union([\n        literal(\"visual\"),\n        literal(\"compact\"),\n        literal(\"raw\")\n      ]),\n      description(\"How linting messages are displayed.\")\n    ),\n    env.CI === \"true\" || env.CI === \"1\"\n      ? \"compact\"\n      : \"visual\"\n  )\n});\n\nexport type MessageStyleOption = InferOutput<typeof MESSAGE_STYLE_OPTION_SCHEMA>;\n\nexport const DETECT_COMPONENT_CLASSES_OPTION_SCHEMA = strictObject({\n  detectComponentClasses: optional(\n    pipe(\n      boolean(),\n      description(\"Whether to automatically detect custom component classes from the tailwindcss config.\")\n    ),\n    false\n  )\n});\n\nexport type DetectComponentClassesOption = InferOutput<typeof DETECT_COMPONENT_CLASSES_OPTION_SCHEMA>;\n\nexport const ROOT_FONT_SIZE_OPTION_SCHEMA = strictObject({\n  rootFontSize: optional(\n    pipe(\n      number(),\n      description(\"The root font size in pixels.\")\n    ),\n    undefined\n  )\n});\n\nexport type RootFontSizeOption = InferOutput<typeof ROOT_FONT_SIZE_OPTION_SCHEMA>;\n\nexport const CWD_OPTION_SCHEMA = strictObject({\n  cwd: optional(\n    pipe(\n      string(),\n      description(\"The working directory to resolve tailwindcss and the config from. Useful in monorepo setups.\")\n    ),\n    undefined\n  )\n});\n\nexport type CwdOption = InferOutput<typeof CWD_OPTION_SCHEMA>;\n"
  },
  {
    "path": "src/options/schemas/matchers.ts",
    "content": "import { description, literal, optional, pipe, strictObject, string } from \"valibot\";\n\nimport { MatcherType } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const STRING_MATCHER_SCHEMA = strictObject({\n  match: pipe(\n    literal(MatcherType.String),\n    description(\"Matcher type that will be applied.\")\n  )\n});\n\nexport const OBJECT_KEY_MATCHER_SCHEMA = strictObject({\n  match: pipe(\n    literal(MatcherType.ObjectKey),\n    description(\"Matcher type that will be applied.\")\n  ),\n  pathPattern: optional(pipe(\n    string(),\n    description(\"Regular expression that filters the object key and matches the content for further processing in a group.\")\n  ))\n});\n\nexport const OBJECT_VALUE_MATCHER_SCHEMA = strictObject({\n  match: pipe(\n    literal(MatcherType.ObjectValue),\n    description(\"Matcher type that will be applied.\")\n  ),\n  pathPattern: optional(pipe(\n    string(),\n    description(\"Regular expression that filters the object value and matches the content for further processing in a group.\")\n  ))\n});\n"
  },
  {
    "path": "src/options/schemas/selectors.ts",
    "content": "import {\n  array,\n  description,\n  literal,\n  number,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  union\n} from \"valibot\";\n\nimport { DEFAULT_SELECTORS } from \"better-tailwindcss:options/default-options.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nconst STRING_SELECTOR_MATCHER_SCHEMA = strictObject({\n  type: pipe(\n    literal(MatcherType.String),\n    description(\"Matcher type that will be applied.\")\n  )\n});\n\nconst OBJECT_KEY_SELECTOR_MATCHER_SCHEMA = strictObject({\n  path: optional(pipe(\n    string(),\n    description(\"Regular expression that filters the object key and matches the content for further processing in a group.\")\n  )),\n  type: pipe(\n    literal(MatcherType.ObjectKey),\n    description(\"Matcher type that will be applied.\")\n  )\n});\n\nconst OBJECT_VALUE_SELECTOR_MATCHER_SCHEMA = strictObject({\n  path: optional(pipe(\n    string(),\n    description(\"Regular expression that filters the object value and matches the content for further processing in a group.\")\n  )),\n  type: pipe(\n    literal(MatcherType.ObjectValue),\n    description(\"Matcher type that will be applied.\")\n  )\n});\n\nconst ANONYMOUS_FUNCTION_RETURN_SELECTOR_MATCHER_SCHEMA = strictObject({\n  match: pipe(\n    array(union([\n      STRING_SELECTOR_MATCHER_SCHEMA,\n      OBJECT_KEY_SELECTOR_MATCHER_SCHEMA,\n      OBJECT_VALUE_SELECTOR_MATCHER_SCHEMA\n    ])),\n    description(\"List of nested matchers that target the return value of anonymous functions.\")\n  ),\n  type: pipe(\n    literal(MatcherType.AnonymousFunctionReturn),\n    description(\"Matcher type that will be applied.\")\n  )\n});\n\n\nconst SELECTOR_MATCH_SCHEMA = pipe(\n  optional(\n    array(\n      union([\n        STRING_SELECTOR_MATCHER_SCHEMA,\n        OBJECT_KEY_SELECTOR_MATCHER_SCHEMA,\n        OBJECT_VALUE_SELECTOR_MATCHER_SCHEMA,\n        ANONYMOUS_FUNCTION_RETURN_SELECTOR_MATCHER_SCHEMA\n      ])\n    )\n  ),\n  description(\"Optional list of matchers that will be applied.\")\n);\n\nconst SELECTOR_NAME_SCHEMA = pipe(\n  string(),\n  description(\"Regular expression for names that should be linted.\")\n);\n\nconst CALLEE_SELECTOR_PATH_SCHEMA = pipe(\n  string(),\n  description(\"Regular expression for callee paths that should be linted.\")\n);\n\nconst TAG_SELECTOR_PATH_SCHEMA = pipe(\n  string(),\n  description(\"Regular expression for tag paths that should be linted.\")\n);\n\nconst CALLEE_SELECTOR_TARGET_VALUE_SCHEMA = optional(\n  union([\n    literal(\"all\"),\n    literal(\"first\"),\n    literal(\"last\"),\n    number()\n  ])\n);\n\nconst CALLEE_SELECTOR_TARGET_ARGUMENT_SCHEMA = pipe(\n  CALLEE_SELECTOR_TARGET_VALUE_SCHEMA,\n  description(\"Optional argument target for call arguments: index, first, last, or all.\")\n);\n\nconst CALLEE_SELECTOR_TARGET_CALL_SCHEMA = pipe(\n  CALLEE_SELECTOR_TARGET_VALUE_SCHEMA,\n  description(\"Optional call target for curried callees: index, first, last, or all.\")\n);\n\nconst CALLEE_SELECTOR_LEGACY_CALL_TARGET_SCHEMA = pipe(\n  optional(\n    union([\n      literal(\"all\"),\n      literal(\"first\"),\n      literal(\"last\"),\n      number()\n    ])\n  ),\n  description(\"Optional call target for curried callees: index, first, last, or all.\")\n);\n\nconst ATTRIBUTE_SELECTOR_SCHEMA = strictObject({\n  kind: pipe(\n    literal(SelectorKind.Attribute),\n    description(\"Selector kind that determines where matching is applied.\")\n  ),\n  match: SELECTOR_MATCH_SCHEMA,\n  name: SELECTOR_NAME_SCHEMA\n});\n\nconst CALLEE_SELECTOR_SCHEMA = union([\n  strictObject({\n    callTarget: CALLEE_SELECTOR_LEGACY_CALL_TARGET_SCHEMA,\n    kind: pipe(\n      literal(SelectorKind.Callee),\n      description(\"Selector kind that determines where matching is applied.\")\n    ),\n    match: SELECTOR_MATCH_SCHEMA,\n    name: SELECTOR_NAME_SCHEMA,\n    path: optional(CALLEE_SELECTOR_PATH_SCHEMA),\n    targetArgument: CALLEE_SELECTOR_TARGET_ARGUMENT_SCHEMA,\n    targetCall: CALLEE_SELECTOR_TARGET_CALL_SCHEMA\n  }),\n  strictObject({\n    callTarget: CALLEE_SELECTOR_LEGACY_CALL_TARGET_SCHEMA,\n    kind: pipe(\n      literal(SelectorKind.Callee),\n      description(\"Selector kind that determines where matching is applied.\")\n    ),\n    match: SELECTOR_MATCH_SCHEMA,\n    name: optional(SELECTOR_NAME_SCHEMA),\n    path: CALLEE_SELECTOR_PATH_SCHEMA,\n    targetArgument: CALLEE_SELECTOR_TARGET_ARGUMENT_SCHEMA,\n    targetCall: CALLEE_SELECTOR_TARGET_CALL_SCHEMA\n  })\n]);\n\nconst TAG_SELECTOR_SCHEMA = union([\n  strictObject({\n    kind: pipe(\n      literal(SelectorKind.Tag),\n      description(\"Selector kind that determines where matching is applied.\")\n    ),\n    match: SELECTOR_MATCH_SCHEMA,\n    name: SELECTOR_NAME_SCHEMA,\n    path: optional(TAG_SELECTOR_PATH_SCHEMA)\n  }),\n  strictObject({\n    kind: pipe(\n      literal(SelectorKind.Tag),\n      description(\"Selector kind that determines where matching is applied.\")\n    ),\n    match: SELECTOR_MATCH_SCHEMA,\n    name: optional(SELECTOR_NAME_SCHEMA),\n    path: TAG_SELECTOR_PATH_SCHEMA\n  })\n]);\n\nconst VARIABLE_SELECTOR_SCHEMA = strictObject({\n  kind: pipe(\n    literal(SelectorKind.Variable),\n    description(\"Selector kind that determines where matching is applied.\")\n  ),\n  match: SELECTOR_MATCH_SCHEMA,\n  name: SELECTOR_NAME_SCHEMA\n});\n\nexport const SELECTOR_SCHEMA = union([\n  ATTRIBUTE_SELECTOR_SCHEMA,\n  CALLEE_SELECTOR_SCHEMA,\n  TAG_SELECTOR_SCHEMA,\n  VARIABLE_SELECTOR_SCHEMA\n]);\n\nexport type Selector = InferOutput<typeof SELECTOR_SCHEMA>;\n\nexport const SELECTORS_SCHEMA = pipe(\n  array(SELECTOR_SCHEMA),\n  description(\"Flat list of selectors that should get linted.\")\n);\n\nexport type Selectors = InferOutput<typeof SELECTORS_SCHEMA>;\n\nexport const SELECTORS_OPTION_SCHEMA = strictObject({\n  selectors: optional(SELECTORS_SCHEMA, DEFAULT_SELECTORS)\n});\n\nexport type SelectorsOptions = InferOutput<typeof SELECTORS_OPTION_SCHEMA>;\n"
  },
  {
    "path": "src/options/schemas/tags.ts",
    "content": "import {\n  array,\n  description,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  tuple,\n  union\n} from \"valibot\";\n\nimport {\n  OBJECT_KEY_MATCHER_SCHEMA,\n  OBJECT_VALUE_MATCHER_SCHEMA,\n  STRING_MATCHER_SCHEMA\n} from \"better-tailwindcss:options/schemas/matchers.js\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nconst TAG_MATCHER_CONFIG = pipe(\n  tuple([\n    pipe(\n      string(),\n      description(\"Template literal tag for which children get linted if matched.\")\n    ),\n    pipe(\n      array(\n        union([\n          STRING_MATCHER_SCHEMA,\n          OBJECT_KEY_MATCHER_SCHEMA,\n          OBJECT_VALUE_MATCHER_SCHEMA\n        ])\n      ),\n      description(\"List of matchers that will be applied.\")\n    )\n  ]),\n  description(\"List of matchers that will automatically be matched.\")\n);\n\nexport type TagMatchers = InferOutput<typeof TAG_MATCHER_CONFIG>;\n\nconst TAG_NAME_CONFIG = pipe(\n  string(),\n  description(\"Template literal tag that should get linted.\")\n);\n\nexport type TagName = InferOutput<typeof TAG_NAME_CONFIG>;\n\nexport const TAGS_SCHEMA = pipe(\n  array(\n    union([\n      TAG_MATCHER_CONFIG,\n      TAG_NAME_CONFIG\n    ])\n  ),\n  description(\"List of template literal tags that should get linted.\")\n);\n\nexport type Tags = InferOutput<typeof TAGS_SCHEMA>;\n\nexport const TAGS_OPTIONS_SCHEMA = strictObject({\n  tags: optional(TAGS_SCHEMA)\n});\n\nexport type TagsOptions = InferOutput<typeof TAGS_OPTIONS_SCHEMA>;\n"
  },
  {
    "path": "src/options/schemas/variables.ts",
    "content": "import {\n  array,\n  description,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  tuple,\n  union\n} from \"valibot\";\n\nimport {\n  OBJECT_KEY_MATCHER_SCHEMA,\n  OBJECT_VALUE_MATCHER_SCHEMA,\n  STRING_MATCHER_SCHEMA\n} from \"better-tailwindcss:options/schemas/matchers.js\";\n\nimport type { InferOutput } from \"valibot\";\n\n\nexport const VARIABLE_MATCHER_CONFIG = pipe(\n  tuple([\n    pipe(\n      string(),\n      description(\"Variable name for which children get linted if matched.\")\n    ),\n    pipe(\n      array(\n        union([\n          STRING_MATCHER_SCHEMA,\n          OBJECT_KEY_MATCHER_SCHEMA,\n          OBJECT_VALUE_MATCHER_SCHEMA\n        ])\n      ),\n      description(\"List of matchers that will be applied.\")\n    )\n  ]),\n  description(\"List of matchers that will automatically be matched.\")\n);\n\nexport type VariableMatchers = InferOutput<typeof VARIABLE_MATCHER_CONFIG>;\n\nexport const VARIABLE_NAME_CONFIG = pipe(\n  string(),\n  description(\"Variable name for which children get linted.\")\n);\n\nexport type VariableName = InferOutput<typeof VARIABLE_NAME_CONFIG>;\n\nexport const VARIABLES_SCHEMA = pipe(\n  array(\n    union([\n      VARIABLE_MATCHER_CONFIG,\n      VARIABLE_NAME_CONFIG\n    ])\n  ),\n  description(\"List of variable names which values should get linted.\")\n);\n\nexport type Variables = InferOutput<typeof VARIABLES_SCHEMA>;\n\nexport const VARIABLES_OPTION_SCHEMA = strictObject({\n  variables: optional(VARIABLES_SCHEMA)\n});\n\nexport type VariablesOptions = InferOutput<typeof VARIABLES_OPTION_SCHEMA>;\n"
  },
  {
    "path": "src/options/tags/twc.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { TWC_CALLEE_STRINGS, TWC_TAG } from \"better-tailwindcss:options/tags/twc.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"twc\", () => {\n\n  it(\"should lint tagged template literals on member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"twc.div` lint `;\",\n          jsxOutput: \"twc.div`lint`;\",\n          svelte: \"<script>twc.div` lint `;</script>\",\n          svelteOutput: \"<script>twc.div`lint`;</script>\",\n          vue: \"<script>twc.div` lint `;</script>\",\n          vueOutput: \"<script>twc.div`lint`;</script>\",\n\n          errors: 2,\n          options: [{ selectors: [TWC_TAG] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint tagged template literals on call expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"twc(Card)` lint `;\",\n          jsxOutput: \"twc(Card)`lint`;\",\n          svelte: \"<script>twc(Card)` lint `;</script>\",\n          svelteOutput: \"<script>twc(Card)`lint`;</script>\",\n          vue: \"<script>twc(Card)` lint `;</script>\",\n          vueOutput: \"<script>twc(Card)`lint`;</script>\",\n\n          errors: 2,\n          options: [{ selectors: [TWC_TAG] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings inside arrow function callbacks\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `twc.div(({ $active }) => [\" lint \", $active && \" lint \"]);`,\n          jsxOutput: `twc.div(({ $active }) => [\"lint\", $active && \"lint\"]);`,\n          svelte: `<script>twc.div(({ $active }) => [\" lint \", $active && \" lint \"]);</script>`,\n          svelteOutput: `<script>twc.div(({ $active }) => [\"lint\", $active && \"lint\"]);</script>`,\n          vue: `<script>twc.div(({ $active }) => [\" lint \", $active && \" lint \"]);</script>`,\n          vueOutput: `<script>twc.div(({ $active }) => [\"lint\", $active && \"lint\"]);</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TWC_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings inside arrow function callbacks with block body\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `twc.div(({ $active }) => { return [\" lint \", $active && \" lint \"]; });`,\n          jsxOutput: `twc.div(({ $active }) => { return [\"lint\", $active && \"lint\"]; });`,\n          svelte: `<script>twc.div(({ $active }) => { return [\" lint \", $active && \" lint \"]; });</script>`,\n          svelteOutput: `<script>twc.div(({ $active }) => { return [\"lint\", $active && \"lint\"]; });</script>`,\n          vue: `<script>twc.div(({ $active }) => { return [\" lint \", $active && \" lint \"]; });</script>`,\n          vueOutput: `<script>twc.div(({ $active }) => { return [\"lint\", $active && \"lint\"]; });</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TWC_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings in conditional arrow function returns\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `twc.div(({ $active }) => $active ? \" lint \" : \" lint2 \");`,\n          jsxOutput: `twc.div(({ $active }) => $active ? \"lint\" : \"lint2\");`,\n          svelte: `<script>twc.div(({ $active }) => $active ? \" lint \" : \" lint2 \");</script>`,\n          svelteOutput: `<script>twc.div(({ $active }) => $active ? \"lint\" : \"lint2\");</script>`,\n          vue: `<script>twc.div(({ $active }) => $active ? \" lint \" : \" lint2 \");</script>`,\n          vueOutput: `<script>twc.div(({ $active }) => $active ? \"lint\" : \"lint2\");</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TWC_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings with multiple return paths in block body\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `twc.div(({ $active }) => { if($active) { return \" lint \"; } return \" lint2 \"; });`,\n          jsxOutput: `twc.div(({ $active }) => { if($active) { return \"lint\"; } return \"lint2\"; });`,\n\n          errors: 4,\n          options: [{ selectors: [TWC_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not lint strings that are not inside twc member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `other.div(({ $active }) => [\" ignore \", $active && \" ignore \"]);`,\n          svelte: `<script>other.div(({ $active }) => [\" ignore \", $active && \" ignore \"]);</script>`,\n          vue: `<script>other.div(({ $active }) => [\" ignore \", $active && \" ignore \"]);</script>`,\n\n          options: [{ selectors: [TWC_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/tags/twc.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors, TagSelector } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const TWC_TAG = {\n  kind: SelectorKind.Tag,\n  path: \"twc(\\\\.\\\\w+)?\"\n} satisfies TagSelector;\n\nexport const TWC_CALLEE_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    },\n    {\n      match: [{\n        type: MatcherType.String\n      }],\n      type: MatcherType.AnonymousFunctionReturn\n    }\n  ],\n  path: \"^twc\\\\.\\\\w+\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/gregberge/twc */\nexport const TWC = [\n  TWC_TAG,\n  TWC_CALLEE_STRINGS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/options/tags/twx.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { TWX_CALLEE_STRINGS, TWX_TAG } from \"better-tailwindcss:options/tags/twx.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"twx\", () => {\n\n  it(\"should lint tagged template literals on member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"twx.div` lint `;\",\n          jsxOutput: \"twx.div`lint`;\",\n          svelte: \"<script>twx.div` lint `;</script>\",\n          svelteOutput: \"<script>twx.div`lint`;</script>\",\n          vue: \"<script>twx.div` lint `;</script>\",\n          vueOutput: \"<script>twx.div`lint`;</script>\",\n\n          errors: 2,\n          options: [{ selectors: [TWX_TAG] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint tagged template literals on call expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"twx(Card)` lint `;\",\n          jsxOutput: \"twx(Card)`lint`;\",\n          svelte: \"<script>twx(Card)` lint `;</script>\",\n          svelteOutput: \"<script>twx(Card)`lint`;</script>\",\n          vue: \"<script>twx(Card)` lint `;</script>\",\n          vueOutput: \"<script>twx(Card)`lint`;</script>\",\n\n          errors: 2,\n          options: [{ selectors: [TWX_TAG] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings inside arrow function callbacks\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `twx.div(({ $active }) => [\" lint \", $active && \" lint \"]);`,\n          jsxOutput: `twx.div(({ $active }) => [\"lint\", $active && \"lint\"]);`,\n          svelte: `<script>twx.div(({ $active }) => [\" lint \", $active && \" lint \"]);</script>`,\n          svelteOutput: `<script>twx.div(({ $active }) => [\"lint\", $active && \"lint\"]);</script>`,\n          vue: `<script>twx.div(({ $active }) => [\" lint \", $active && \" lint \"]);</script>`,\n          vueOutput: `<script>twx.div(({ $active }) => [\"lint\", $active && \"lint\"]);</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TWX_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings inside arrow function callbacks with block body\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `twx.span(({ $active }) => { return [\" lint \", $active && \" lint \"]; });`,\n          jsxOutput: `twx.span(({ $active }) => { return [\"lint\", $active && \"lint\"]; });`,\n          svelte: `<script>twx.span(({ $active }) => { return [\" lint \", $active && \" lint \"]; });</script>`,\n          svelteOutput: `<script>twx.span(({ $active }) => { return [\"lint\", $active && \"lint\"]; });</script>`,\n          vue: `<script>twx.span(({ $active }) => { return [\" lint \", $active && \" lint \"]; });</script>`,\n          vueOutput: `<script>twx.span(({ $active }) => { return [\"lint\", $active && \"lint\"]; });</script>`,\n\n          errors: 4,\n          options: [{ selectors: [TWX_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not lint strings that are not inside twx member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `other.div(({ $active }) => [\" ignore \", $active && \" ignore \"]);`,\n          svelte: `<script>other.div(({ $active }) => [\" ignore \", $active && \" ignore \"]);</script>`,\n          vue: `<script>other.div(({ $active }) => [\" ignore \", $active && \" ignore \"]);</script>`,\n\n          options: [{ selectors: [TWX_CALLEE_STRINGS] }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/options/tags/twx.ts",
    "content": "import { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\nimport type { CalleeSelector, Selectors, TagSelector } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const TWX_TAG = {\n  kind: SelectorKind.Tag,\n  path: \"twx(\\\\.\\\\w+)?\"\n} satisfies TagSelector;\n\nexport const TWX_CALLEE_STRINGS = {\n  kind: SelectorKind.Callee,\n  match: [\n    {\n      type: MatcherType.String\n    },\n    {\n      match: [{\n        type: MatcherType.String\n      }],\n      type: MatcherType.AnonymousFunctionReturn\n    }\n  ],\n  path: \"^twx\\\\.\\\\w+\"\n} satisfies CalleeSelector;\n\n/** @see https://github.com/gregberge/twc */\nexport const TWX = [\n  TWX_TAG,\n  TWX_CALLEE_STRINGS\n] satisfies Selectors;\n"
  },
  {
    "path": "src/parsers/angular.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { noRestrictedClasses } from \"better-tailwindcss:rules/no-restricted-classes.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\nimport { MatcherType } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"angular\", () => {\n\n  describe(\"defaults\", () => {\n    it(\"should support normal classes\", () => {\n      lint(enforceConsistentClassOrder, {\n        invalid: [\n          {\n            angular: `<img class=\"b a\" />`,\n            angularOutput: `<img class=\"a b\" />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img [class]=\"'b a'\" />`,\n            angularOutput: `<img [class]=\"'a b'\" />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img [ngClass]=\"'b a'\" />`,\n            angularOutput: `<img [ngClass]=\"'a b'\" />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      });\n    });\n\n    it(\"should support array binding in [class] and [ngClass]\", () => {\n      lint(enforceConsistentClassOrder, {\n        invalid: [\n          {\n            angular: `<img [class]=\"['b a', 'd c']\" />`,\n            angularOutput: `<img [class]=\"['a b', 'c d']\" />`,\n\n            errors: 2,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img [ngClass]=\"['b a', 'd c']\" />`,\n            angularOutput: `<img [ngClass]=\"['a b', 'c d']\" />`,\n\n            errors: 2,\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      });\n    });\n\n    it(\"should support expressions in literal arrays\", () => {\n      lint(enforceConsistentClassOrder, {\n        invalid: [\n          {\n            angular: `<img [class]=\"['b a', expression ? 'd c' : 'f e']\" />`,\n            angularOutput: `<img [class]=\"['a b', expression ? 'c d' : 'e f']\" />`,\n\n            errors: 3,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img [ngClass]=\"['b a', expression ? 'd c' : 'f e']\" />`,\n            angularOutput: `<img [ngClass]=\"['a b', expression ? 'c d' : 'e f']\" />`,\n\n            errors: 3,\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      });\n    });\n\n    it(\"should support object keys in [class] and [ngClass]\", () => {\n      lint(enforceConsistentClassOrder, {\n        invalid: [\n          {\n            angular: `<img [class]=\"{ 'b a': true, 'd c': false }\" />`,\n            angularOutput: `<img [class]=\"{ 'a b': true, 'c d': false }\" />`,\n\n            errors: 2,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img [ngClass]=\"{ 'b a': true, 'd c': false }\" />`,\n            angularOutput: `<img [ngClass]=\"{ 'a b': true, 'c d': false }\" />`,\n\n            errors: 2,\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      });\n    });\n  });\n\n  describe(\"names\", () => {\n    it(\"should match attribute names via names regex\", () => {\n      lint(enforceConsistentClassOrder, {\n        invalid: [\n          {\n            angular: `<img customAttribute=\"b a\" />`,\n            angularOutput: `<img customAttribute=\"a b\" />`,\n\n            errors: 1,\n            options: [{ attributes: [\".*Attribute\"], order: \"asc\" }]\n          },\n          {\n            angular: `<img class=\"b a\" />`,\n            angularOutput: `<img class=\"a b\" />`,\n\n            errors: 1,\n            options: [{ attributes: [\"class\"], order: \"asc\" }]\n          },\n          {\n            angular: `<img [class]=\"'b a'\" />`,\n            angularOutput: `<img [class]=\"'a b'\" />`,\n\n            errors: 1,\n            options: [{ attributes: [\"\\\\[class\\\\]\"], order: \"asc\" }]\n          },\n          {\n            angular: `<img [ngClass]=\"'b a'\" />`,\n            angularOutput: `<img [ngClass]=\"'a b'\" />`,\n\n            errors: 1,\n            options: [{ attributes: [\"\\\\[ngClass\\\\]\"], order: \"asc\" }]\n          }\n        ]\n      });\n    });\n  });\n\n  describe(\"matchers\", () => {\n\n    describe(\"string\", () => {\n      it(\"should match attribute names via matchers\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img class=\"b a\" />`,\n              angularOutput: `<img class=\"a b\" />`,\n\n              errors: 1,\n              options: [{ attributes: [[\"class\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: `<img [class]=\"'b a'\" />`,\n              angularOutput: `<img [class]=\"'a b'\" />`,\n\n              errors: 1,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: `<img [ngClass]=\"'b a'\" />`,\n              angularOutput: `<img [ngClass]=\"'a b'\" />`,\n\n              errors: 1,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            }\n          ]\n        });\n      });\n    });\n\n    describe(\"object keys\", () => {\n      it(\"should match object keys\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [class]=\"{ 'b a': true, 'd c': false }\" />`,\n              angularOutput: `<img [class]=\"{ 'a b': true, 'c d': false }\" />`,\n\n              errors: 2,\n              options: [{\n                attributes: [\n                  [\n                    \"\\\\[class\\\\]\", [\n                      {\n                        match: MatcherType.ObjectKey\n                      }\n                    ]\n                  ]\n                ],\n                order: \"asc\"\n              }]\n            },\n            {\n              angular: `<img [ngClass]=\"{ 'b a': true, 'd c': false }\" />`,\n              angularOutput: `<img [ngClass]=\"{ 'a b': true, 'c d': false }\" />`,\n\n              errors: 2,\n              options: [{\n                attributes: [\n                  [\n                    \"\\\\[ngClass\\\\]\", [\n                      {\n                        match: MatcherType.ObjectKey\n                      }\n                    ]\n                  ]\n                ],\n                order: \"asc\"\n              }]\n            }\n          ]\n        });\n      });\n\n      it(\"should still match the object key when there is a value with the same content\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [class]=\"{ 'b a': 'd c', 'd c': 'b a' }\" />`,\n              angularOutput: `<img [class]=\"{ 'a b': 'd c', 'c d': 'b a' }\" />`,\n\n              errors: 2,\n              options: [{\n                attributes: [\n                  [\n                    \"\\\\[class\\\\]\", [\n                      {\n                        match: MatcherType.ObjectKey\n                      }\n                    ]\n                  ]\n                ],\n                order: \"asc\"\n              }]\n            },\n            {\n              angular: `<img [ngClass]=\"{ 'b a': 'd c', 'd c': 'b a' }\" />`,\n              angularOutput: `<img [ngClass]=\"{ 'a b': 'd c', 'c d': 'b a' }\" />`,\n\n              errors: 2,\n              options: [{\n                attributes: [\n                  [\n                    \"\\\\[ngClass\\\\]\", [\n                      {\n                        match: MatcherType.ObjectKey\n                      }\n                    ]\n                  ]\n                ],\n                order: \"asc\"\n              }]\n            }\n          ]\n        });\n      });\n\n      it(\"should not lint literals in binary comparisons\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [class]=\"{'b a': 'd c' === 'f e'}\" />`,\n              angularOutput: `<img [class]=\"{'a b': 'd c' === 'f e'}\" />`,\n\n              errors: 1,\n              options: [{ order: \"asc\" }]\n            }\n          ]\n        });\n      });\n    });\n\n    describe(\"object values\", () => {\n      // this is not used by angular, but matchers should still be able to handle it\n      it(\"should support object values\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [ngClass]=\"{ '0': 'b a', '1': 'd c' }\" />`,\n              angularOutput: `<img [ngClass]=\"{ '0': 'a b', '1': 'c d' }\" />`,\n\n              errors: 2,\n              options: [{\n                attributes: [[\"\\\\[ngClass\\\\]\", [\n                  {\n                    match: MatcherType.ObjectValue\n                  }\n                ]]],\n                order: \"asc\"\n              }]\n            }\n          ]\n        });\n      });\n    });\n\n    describe(\"arrays\", () => {\n      it(\"should support arrays\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [class]=\"['b a', 'd c']\" />`,\n              angularOutput: `<img [class]=\"['a b', 'c d']\" />`,\n\n              errors: 2,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: `<img [ngClass]=\"['b a', 'd c']\" />`,\n              angularOutput: `<img [ngClass]=\"['a b', 'c d']\" />`,\n\n              errors: 2,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            }\n          ]\n        });\n      });\n\n      it(\"should support expressions in arrays\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [class]=\"['b a', expression ? 'd c' : 'f e']\" />`,\n              angularOutput: `<img [class]=\"['a b', expression ? 'c d' : 'e f']\" />`,\n\n              errors: 3,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: `<img [ngClass]=\"['b a', expression ? 'd c' : 'f e']\" />`,\n              angularOutput: `<img [ngClass]=\"['a b', expression ? 'c d' : 'e f']\" />`,\n\n              errors: 3,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            }\n          ]\n        });\n      });\n    });\n\n    describe(\"expressions\", () => {\n      it(\"should lint classes returned from expressions\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img class=\"{{ true === 'b a' ? 'b a' : 'd c' }}\" />`,\n              angularOutput: `<img class=\"{{ true === 'b a' ? 'a b' : 'c d' }}\" />`,\n\n              errors: 2,\n              options: [{ attributes: [[\"class\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: `<img [class]=\"true === 'b a' ? 'b a' : 'd c'\" />`,\n              angularOutput: `<img [class]=\"true === 'b a' ? 'a b' : 'c d'\" />`,\n\n              errors: 2,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: `<img [ngClass]=\"true === 'b a' ? 'b a' : 'd c'\" />`,\n              angularOutput: `<img [ngClass]=\"true === 'b a' ? 'a b' : 'c d'\" />`,\n\n              errors: 2,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            }\n          ]\n        });\n      });\n    });\n\n    describe(\"template literals\", () => {\n      it(\"should support template literals in interpolated class\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            // 1st pass of multi pass fix\n            {\n              angular: \"<img class=\\\"{{`b a ${true && 'd c'} f e`}}\\\" />\",\n              angularOutput: \"<img class=\\\"{{`a b ${true && 'c d'} f e`}}\\\" />\",\n\n              errors: 3,\n              options: [{ order: \"asc\" }]\n            },\n\n            // 2nd pass of multi pass fix\n            {\n              angular: \"<img class=\\\"{{`a b ${true && 'c d'} f e`}}\\\" />\",\n              angularOutput: \"<img class=\\\"{{`a b ${true && 'c d'} e f`}}\\\" />\",\n\n              errors: 1,\n              options: [{ order: \"asc\" }]\n            }\n          ]\n        });\n      });\n\n      it(\"should support short circuiting\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: \"<img [class]=\\\"`${'b a' && 'd c'}`\\\" />\",\n              angularOutput: \"<img [class]=\\\"`${'b a' && 'c d'}`\\\" />\",\n\n              errors: 1,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            {\n              angular: \"<img [ngClass]=\\\"`${'b a' && 'd c'}`\\\" />\",\n              angularOutput: \"<img [ngClass]=\\\"`${'b a' && 'c d'}`\\\" />\",\n\n              errors: 1,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            }\n          ]\n        });\n      });\n\n      it(\"should lint classes around expressions\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            // 1st pass of multi pass fix\n            {\n              angular: \"<img [class]=\\\"`b a ${'d c'} f e`\\\" />\",\n              angularOutput: \"<img [class]=\\\"`a b ${'d c'} e f`\\\" />\",\n\n              errors: 3,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            // 2nd pass of multi pass fix\n            {\n              angular: \"<img [class]=\\\"`a b ${'d c'} e f`\\\" />\",\n              angularOutput: \"<img [class]=\\\"`a b ${'c d'} e f`\\\" />\",\n\n              errors: 1,\n              options: [{ attributes: [[\"\\\\[class\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n\n            // 1st pass of multi pass fix\n            {\n              angular: \"<img [ngClass]=\\\"`b a ${'d c'} f e`\\\" />\",\n              angularOutput: \"<img [ngClass]=\\\"`a b ${'d c'} e f`\\\" />\",\n\n              errors: 3,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            },\n            // 2nd pass of multi pass fix\n            {\n              angular: \"<img [ngClass]=\\\"`a b ${'d c'} e f`\\\" />\",\n              angularOutput: \"<img [ngClass]=\\\"`a b ${'c d'} e f`\\\" />\",\n\n              errors: 1,\n              options: [{ attributes: [[\"\\\\[ngClass\\\\]\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n            }\n          ]\n        });\n      });\n\n      it(\"should support multiline template literal types\", () => {\n\n        const dirty = dedent`\n          b a\n          d c\n        `;\n\n        const clean = dedent`\n          a b\n          c d\n        `;\n\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              angular: `<img [class]=\"\\`${dirty}\\`\" />`,\n              angularOutput: `<img [class]=\"\\`${clean}\\`\" />`,\n\n              errors: 1,\n              options: [{ order: \"asc\" }]\n            },\n            {\n              angular: `<img [ngClass]=\"\\`${dirty}\\`\" />`,\n              angularOutput: `<img [ngClass]=\"\\`${clean}\\`\" />`,\n\n              errors: 1,\n              options: [{ order: \"asc\" }]\n            }\n          ]\n        });\n      });\n\n      it(\"should not crash on dynamic [class] expression (no literal)\", () => {\n        lint(enforceConsistentClassOrder, {\n          valid: [\n            {\n              // Dynamic computed class expression – previously could crash parser\n              // when parent.source was unavailable.\n              angular: `<icon [class]=\"styles.icon({ disabled: disabled })\" />`\n            }\n          ]\n        });\n      });\n\n      it(\"should not crash on dynamic [ngClass] expression (no literal)\", () => {\n        lint(enforceConsistentClassOrder, {\n          valid: [\n            {\n              angular: `<icon [ngClass]=\"styles.icon({ disabled: disabled })\" />`\n            }\n          ]\n        });\n      });\n\n      it(\"should continue handling literal map keys without crashing\", () => {\n        lint(enforceConsistentClassOrder, {\n          invalid: [\n            {\n              // Sanity check: object-literal keys are still parsed and reordered\n              angular: `<img [class]=\"{ 'b a': true, 'd c': false }\" />`,\n              angularOutput: `<img [class]=\"{ 'a b': true, 'c d': false }\" />`,\n\n              errors: 2,\n              options: [{ order: \"asc\" }]\n            }\n          ]\n        });\n      });\n    });\n\n  });\n\n  // #177\n  it(\"should be able to differentiate between overlapping object keys\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img [class]=\"{\n            'lg:rounded-r-lg ': true,\n            'rounded-r-lg ': true,\n          }\" />`,\n          angularOutput: `<img [class]=\"{\n            'lg:rounded-r-lg': true,\n            'rounded-r-lg': true,\n          }\" />`,\n\n          errors: 2\n        }\n      ]\n    });\n  });\n\n  it(\"should correctly create object paths\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          angular: `<img [class]=\"{\n            'root': {\n              'nested': {\n                'level-2': [{\n                    'matched': 'b a',\n                    'ignored': 'b a',\n                  }]\n              },\n            },\n          }\" />`,\n          angularOutput: `<img [class]=\"{\n            'root': {\n              'nested': {\n                'level-2': [{\n                    'matched': 'a b',\n                    'ignored': 'b a',\n                  }]\n              },\n            },\n          }\" />`,\n\n          errors: 1,\n          options: [{\n            attributes: [\n              [\"\\\\[class\\\\]\", [\n                {\n                  match: MatcherType.ObjectValue,\n                  pathPattern: `root.nested\\\\[\"level-2\"\\\\]\\\\[\\\\d+\\\\].matched`\n                }\n              ]]\n            ],\n            order: \"asc\"\n          }]\n        }\n      ]\n    });\n  });\n\n  // #274\n  it(\"should support bound attribute names\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img [class.restricted]=\"true\" />`,\n          angularOutput: `<img [class.allowed]=\"true\" />`,\n\n          errors: 1,\n          options: [{\n            restrict: [\n              { fix: \"allowed\", pattern: \"restricted\" }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/parsers/angular.ts",
    "content": "import { MATCHER_RESULT, MatcherType } from \"better-tailwindcss:types/rule.js\";\nimport { getLocByRange } from \"better-tailwindcss:utils/ast.js\";\nimport { getLiteralNodesByMatchers, matchesPathPattern } from \"better-tailwindcss:utils/matchers.js\";\nimport {\n  addAttribute,\n  createObjectPathElement,\n  deduplicateLiterals,\n  getIndentation,\n  getQuotes,\n  getWhitespace,\n  matchesName\n} from \"better-tailwindcss:utils/utils.js\";\n\nimport type {\n  AST,\n  ASTWithSource,\n  Binary,\n  Call,\n  Conditional,\n  Interpolation,\n  LiteralArray,\n  LiteralMap,\n  LiteralMapPropertyKey,\n  LiteralPrimitive,\n  ParseSourceSpan,\n  TemplateLiteral,\n  TemplateLiteralElement,\n  TmplAstBoundAttribute,\n  TmplAstElement,\n  TmplAstNode,\n  TmplAstTextAttribute\n} from \"@angular/compiler\";\nimport type { Rule } from \"eslint\";\nimport type { SourceLocation } from \"estree\";\n\nimport type { BracesMeta, Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { AttributeSelector, MatcherFunctions, SelectorMatcher } from \"better-tailwindcss:types/rule.js\";\n\n// https://angular.dev/api/common/NgClass\n// https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings\n\nexport function getAttributesByAngularElement(ctx: Rule.RuleContext, node: TmplAstElement): (TmplAstBoundAttribute | TmplAstTextAttribute)[] {\n  return [\n    ...node.attributes,\n    ...node.inputs\n  ];\n}\n\nexport function getLiteralsByAngularAttribute(ctx: Rule.RuleContext, attribute: TmplAstBoundAttribute | TmplAstTextAttribute, selectors: AttributeSelector[]): Literal[] {\n\n  const name = getAttributeName(attribute);\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n    if(!matchesName(selector.name.toLowerCase(), name.toLowerCase())){ return literals; }\n\n    if(!selector.match){\n      literals.push(...createLiteralsByAngularAttribute(ctx, attribute));\n      return literals;\n    }\n\n    if(isTextAttribute(attribute) && selector.match.some(matcher => matcher.type === MatcherType.String)){\n      literals.push(...createLiteralsByAngularTextAttribute(ctx, attribute));\n    }\n    if(isBoundAttribute(attribute)){\n      if(isBoundAttributeName(attribute)){\n        literals.push(...getLiteralsByAngularMatchers(ctx, attribute, selector.match));\n      } else if(isASTWithSource(attribute.value)){\n        literals.push(...getLiteralsByAngularMatchers(ctx, attribute.value.ast, selector.match));\n      }\n    }\n\n    return literals;\n  }, []);\n\n  return literals\n    .filter(deduplicateLiterals)\n    .map(addAttribute(name));\n}\n\nfunction createLiteralsByAngularAst(ctx: Rule.RuleContext, ast: AST): Literal[] {\n  if(isInterpolation(ast)){\n    return ast.expressions.flatMap(expression => {\n      return createLiteralsByAngularAst(ctx, expression);\n    });\n  }\n\n  if(isLiteralArray(ast)){\n    return ast.expressions.flatMap(expression => {\n      return createLiteralsByAngularAst(ctx, expression);\n    });\n  }\n\n  if(isObjectKey(ast)){\n    return createLiteralByLiteralMapKey(ctx, ast);\n  }\n\n  if(isConditional(ast)){\n    return createLiteralsByAngularConditional(ctx, ast);\n  }\n\n  if(isLiteralPrimitive(ast)){\n    return createLiteralByAngularLiteralPrimitive(ctx, ast);\n  }\n\n  if(isTemplateLiteralElement(ast)){\n    return createLiteralByAngularTemplateLiteralElement(ctx, ast);\n  }\n\n  if(isBoundAttribute(ast) && isBoundAttributeName(ast)){\n    return createLiteralsByAngularBoundAttributeName(ctx, ast);\n  }\n  return [];\n\n}\n\nfunction createLiteralsByAngularConditional(ctx: Rule.RuleContext, conditional: Conditional): Literal[] {\n  const literals: Literal[] = [];\n\n  literals.push(...createLiteralsByAngularAst(ctx, conditional.trueExp));\n  literals.push(...createLiteralsByAngularAst(ctx, conditional.falseExp));\n\n  return literals;\n}\n\nfunction createLiteralsByAngularAttribute(ctx: Rule.RuleContext, attribute: TmplAstBoundAttribute | TmplAstTextAttribute): Literal[] {\n  if(isTextAttribute(attribute)){\n    return createLiteralsByAngularTextAttribute(ctx, attribute);\n  }\n  if(isBoundAttribute(attribute) && isASTWithSource(attribute.value) && isLiteralPrimitive(attribute.value.ast)){\n    return createLiteralsByAngularAst(ctx, attribute.value.ast);\n  }\n  return [];\n}\n\nfunction getLiteralsByAngularMatchers(ctx: Rule.RuleContext, ast: AST | TmplAstBoundAttribute, matchers: SelectorMatcher[]): Literal[] {\n  const matcherFunctions = getAngularMatcherFunctions(ctx, matchers);\n\n  const matchingAstNodes = getLiteralNodesByMatchers<AST>(ctx, ast, matcherFunctions);\n  const literals = matchingAstNodes.flatMap(ast => createLiteralsByAngularAst(ctx, ast));\n\n  return literals.filter(deduplicateLiterals);\n}\n\nfunction getAngularMatcherFunctions(ctx: Rule.RuleContext, matchers: SelectorMatcher[]): MatcherFunctions {\n  return matchers.reduce<MatcherFunctions>((matcherFunctions, matcher) => {\n    switch (matcher.type){\n      case MatcherType.String: {\n        matcherFunctions.push(ast => {\n\n          if(\n            isAST(ast) &&\n            isCallExpression(ast)\n          ){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isAST(ast) ||\n\n            isInsideConditionalExpressionCondition(ctx, ast) ||\n            isInsideLogicalExpressionLeft(ctx, ast) ||\n\n            isObjectKey(ast) ||\n            isInsideObjectValue(ctx, ast)){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          return isStringLike(ast) || isBoundAttributeName(ast);\n        });\n        break;\n      }\n      case MatcherType.ObjectKey: {\n        matcherFunctions.push(ast => {\n\n          if(isAST(ast) && (\n            isCallExpression(ast) ||\n            isBoundAttributeName(ast)\n          )){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isAST(ast) ||\n            !isObjectKey(ast) ||\n\n            isInsideConditionalExpressionCondition(ctx, ast) ||\n            isInsideLogicalExpressionLeft(ctx, ast)){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          const path = getAngularObjectPath(ctx, ast);\n\n          if(!path || !matcher.path){\n            return MATCHER_RESULT.MATCH;\n          }\n\n          return matchesPathPattern(path, matcher.path);\n        });\n        break;\n      }\n      case MatcherType.ObjectValue: {\n        matcherFunctions.push(ast => {\n\n          if(isAST(ast) && (\n            isCallExpression(ast) ||\n            isBoundAttributeName(ast)\n          )){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isAST(ast) ||\n            !hasParent(ast) ||\n            !isInsideObjectValue(ctx, ast) ||\n\n            isInsideConditionalExpressionCondition(ctx, ast) ||\n            isInsideLogicalExpressionLeft(ctx, ast) ||\n            isObjectKey(ast) ||\n\n            !isStringLike(ast)\n          ){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          const path = getAngularObjectPath(ctx, ast);\n\n          if(!path || !matcher.path){\n            return MATCHER_RESULT.MATCH;\n          }\n\n          return matchesPathPattern(path, matcher.path);\n        });\n        break;\n      }\n    }\n    return matcherFunctions;\n  }, []);\n}\n\nfunction getAngularObjectPath(ctx: Rule.RuleContext, ast: AST): string | undefined {\n  const parent = findParent(ctx, ast);\n\n  if(!parent){\n    return;\n  }\n\n  const paths: (string | undefined)[] = [];\n\n  if(isObjectKey(ast)){\n    paths.unshift(createObjectPathElement(ast.key));\n  }\n\n  if(isLiteralArray(parent)){\n    const index = parent.expressions.indexOf(ast);\n    paths.unshift(`[${index}]`);\n  }\n\n  if(isLiteralMap(parent) && isInsideObjectValue(ctx, ast)){\n    const keyIndex = parent.values.indexOf(ast);\n    const objectKey = parent.keys[keyIndex];\n\n    if(objectKey && isObjectKey(objectKey)){\n      paths.unshift(createObjectPathElement(objectKey.key));\n    }\n  }\n\n  paths.unshift(getAngularObjectPath(ctx, parent));\n\n  return paths.reduce<string[]>((paths, currentPath) => {\n    if(!currentPath){ return paths; }\n\n    if(paths.length === 0){\n      return [currentPath];\n    }\n\n    if(currentPath.startsWith(\"[\") && currentPath.endsWith(\"]\")){\n      return [...paths, currentPath];\n    }\n\n    return [...paths, \".\", currentPath];\n  }, []).join(\"\");\n\n}\n\nfunction createLiteralsByAngularBoundAttributeName(ctx: Rule.RuleContext, attribute: TmplAstBoundAttribute): Literal[] {\n\n  if(!attribute.keySpan){\n    return [];\n  }\n\n  const content = attribute.name;\n\n  const startOffset = attribute.keySpan.toString()?.indexOf(content) ?? 0;\n\n  const start = attribute.keySpan.fullStart;\n  const end = attribute.keySpan.end;\n  const range = [start.offset + startOffset, end.offset] satisfies [number, number];\n  const raw = attribute.sourceSpan.start.file.content.slice(...range);\n  const quotes = getQuotes(raw);\n  const whitespaces = getWhitespace(content);\n  const loc = convertParseSourceSpanToLoc(attribute.keySpan);\n\n  loc.start.column += startOffset;\n  loc.end.column = loc.start.column + content.length;\n\n  const line = ctx.sourceCode.lines[loc.start.line - 1];\n  const indentation = getIndentation(line);\n  const supportsMultiline = false;\n  const concatenation = {\n    isConcatenatedLeft: false,\n    isConcatenatedRight: false\n  };\n\n  return [{\n    ...quotes,\n    ...whitespaces,\n    ...concatenation,\n    content,\n    indentation,\n    loc,\n    range,\n    raw,\n    supportsMultiline,\n    type: \"StringLiteral\"\n  }];\n}\n\nfunction createLiteralByLiteralMapKey(ctx: Rule.RuleContext, key: LiteralMapPropertyKey): Literal[] {\n  // @ts-expect-error - angular types are faulty\n  const literalMap = key?.parent as LiteralMap | undefined;\n  // @ts-expect-error - angular types are faulty\n  const objectContent = literalMap?.parent?.source;\n  const keyContent = key?.key;\n  const keyIndex = literalMap?.keys.indexOf(key);\n\n  if(keyIndex === undefined || keyIndex === -1){\n    return [];\n  }\n\n  const previousValue = literalMap?.values[keyIndex - 1];\n  const value = literalMap?.values[keyIndex];\n\n  if(!literalMap?.sourceSpan || typeof objectContent !== \"string\" || typeof keyContent !== \"string\"){\n    return [];\n  }\n\n  const rangeStart = previousValue?.span?.end ?? 0;\n  const rangeEnd = value?.span?.start ?? objectContent.length;\n\n  const slice = objectContent.slice(rangeStart, rangeEnd);\n\n  const start = rangeStart + slice.indexOf(keyContent) - (key.quoted ? 1 : 0);\n  const end = start + keyContent.length + (key.quoted ? 1 : 0);\n\n  const raw = objectContent.slice(start, end);\n  const quotes = getQuotes(raw);\n  const whitespaces = getWhitespace(keyContent);\n  const range = [literalMap.sourceSpan.start + start, literalMap.sourceSpan.start + end] satisfies [number, number];\n  const loc = getLocByRange(ctx, range);\n  const line = ctx.sourceCode.lines[loc.start.line - 1] ?? \"\";\n  const indentation = getIndentation(line);\n  const concatenation = {\n    isConcatenatedLeft: false,\n    isConcatenatedRight: false\n  };\n\n  return [{\n    ...quotes,\n    ...whitespaces,\n    ...concatenation,\n    content: keyContent,\n    indentation,\n    loc,\n    range,\n    raw,\n    supportsMultiline: false,\n    type: \"StringLiteral\"\n  }];\n}\n\nfunction createLiteralsByAngularTextAttribute(ctx: Rule.RuleContext, attribute: TmplAstTextAttribute): Literal[] {\n  const content = attribute.value;\n\n  if(!attribute.valueSpan){\n    return [];\n  }\n\n  const start = attribute.valueSpan.fullStart;\n  const end = attribute.valueSpan.end;\n  const range = [start.offset - 1, end.offset + 1] satisfies [number, number];\n  const raw = attribute.sourceSpan.start.file.content.slice(...range);\n  const quotes = getQuotes(raw);\n  const whitespaces = getWhitespace(content);\n  const loc = convertParseSourceSpanToLoc(attribute.valueSpan);\n  const line = ctx.sourceCode.lines[loc.start.line - 1];\n  const indentation = getIndentation(line);\n  const supportsMultiline = true;\n  const concatenation = {\n    isConcatenatedLeft: false,\n    isConcatenatedRight: false\n  };\n\n  return [{\n    ...quotes,\n    ...whitespaces,\n    ...concatenation,\n    content,\n    indentation,\n    loc,\n    range,\n    raw,\n    supportsMultiline,\n    type: \"StringLiteral\"\n  }];\n}\n\nfunction createLiteralByAngularLiteralPrimitive(ctx: Rule.RuleContext, literal: LiteralPrimitive): Literal[] {\n  const content = literal.value;\n\n  if(!literal.sourceSpan || typeof content !== \"string\"){\n    return [];\n  }\n\n  const start = literal.sourceSpan.start;\n  const end = literal.sourceSpan.end;\n  const range = [start, end] satisfies [number, number];\n  const raw = ctx.sourceCode.text.slice(...range);\n  const quotes = getQuotes(raw);\n  const whitespaces = getWhitespace(content);\n  const loc = getLocByRange(ctx, range);\n  const line = ctx.sourceCode.lines[loc.start.line - 1];\n  const indentation = getIndentation(line);\n  const concatenation = getStringConcatenationMeta(ctx, literal);\n  const supportsMultiline = true;\n\n  return [{\n    ...quotes,\n    ...whitespaces,\n    ...concatenation,\n    content,\n    indentation,\n    loc,\n    range,\n    raw,\n    supportsMultiline,\n    type: \"StringLiteral\"\n  }];\n}\n\nfunction createLiteralByAngularTemplateLiteralElement(ctx: Rule.RuleContext, literal: TemplateLiteralElement): Literal[] {\n  const content = literal.text;\n\n  if(!literal.sourceSpan || !hasParent(literal)){\n    return [];\n  }\n\n  const braces = getBraces(literal);\n  const isInterpolated = getIsInterpolated(literal);\n  const start = literal.sourceSpan.start - (braces.closingBraces?.length ?? 0);\n  const end = literal.sourceSpan.end + (braces.openingBraces?.length ?? 0);\n  const range = [start, end] satisfies [number, number];\n  const raw = ctx.sourceCode.text.slice(...range);\n  const quotes = getQuotes(raw);\n  const whitespaces = getWhitespace(content);\n  const loc = getLocByRange(ctx, range);\n\n  const parent = literal.parent;\n  const parentStart = parent.sourceSpan?.start;\n  const parentEnd = parent.sourceSpan?.end;\n  const parentRange = [parentStart, parentEnd] satisfies [number, number];\n  const parentLoc = getLocByRange(ctx, parentRange);\n  const parentLine = ctx.sourceCode.lines[parentLoc.start.line - 1];\n  const indentation = getIndentation(parentLine);\n  const supportsMultiline = true;\n  const concatenation = getStringConcatenationMeta(ctx, literal);\n\n  return [{\n    ...quotes,\n    ...whitespaces,\n    ...braces,\n    ...concatenation,\n    content,\n    indentation,\n    isInterpolated,\n    loc,\n    range,\n    raw,\n    supportsMultiline,\n    type: \"TemplateLiteral\"\n  }];\n}\n\nfunction convertParseSourceSpanToLoc(sourceSpan: ParseSourceSpan): SourceLocation {\n  return {\n    end: {\n      column: sourceSpan.end.col,\n      line: sourceSpan.end.line + 1\n    },\n    start: {\n      column: sourceSpan.fullStart.col,\n      line: sourceSpan.fullStart.line + 1\n    }\n  };\n}\n\nfunction isInsideInlineTemplate(ctx: Rule.RuleContext) {\n  return getInlineTemplateComponentIndex(ctx) !== undefined;\n}\n\nfunction getInlineTemplateComponentIndex(ctx: Rule.RuleContext) {\n  const matches = ctx.filename.match(/^.*_inline-template-[\\w.-]+-(\\d+)\\.component\\.html$/);\n\n  if(matches){\n    const [, index] = matches;\n    return +index;\n  }\n}\n\nfunction getBraces(literal: TemplateLiteralElement): BracesMeta {\n  if(!hasParent(literal)){\n    return {};\n  }\n\n  const parent = literal.parent as TemplateLiteral;\n  const index = parent.elements.indexOf(literal);\n\n  if(parent.elements.length === 1){\n    return {};\n  }\n\n  return {\n    closingBraces: index >= 1 ? \"}\" : undefined,\n    openingBraces: index < parent.elements.length - 1 ? \"${\" : undefined\n  };\n}\n\nfunction getIsInterpolated(literal: TemplateLiteralElement): boolean {\n  const braces = getBraces(literal);\n  return !!braces.closingBraces || !!braces.openingBraces;\n}\n\nfunction getAttributeName(node: TmplAstBoundAttribute | TmplAstTextAttribute): string {\n  if(!node.keySpan){\n    return node.name;\n  }\n\n  return node.sourceSpan.start.offset !== node.keySpan.start.offset\n    ? node.sourceSpan.fullStart.file.content.slice(node.sourceSpan.start.offset, node.keySpan.end.offset + 1)\n    : node.keySpan.toString() ?? node.name;\n}\n\nexport type Parent = {\n  parent: AST;\n};\n\nfunction isInsideConditionalExpressionCondition(ctx: Rule.RuleContext, ast: AST): boolean {\n  const parent = findParent(ctx, ast);\n  if(!parent){ return false; }\n\n  if(isConditional(parent) && parent.condition === ast){\n    return true;\n  }\n\n  return isInsideConditionalExpressionCondition(ctx, parent);\n}\n\nfunction isInsideLogicalExpressionLeft(ctx: Rule.RuleContext, ast: AST): boolean {\n  const parent = findParent(ctx, ast);\n  if(!parent){ return false; }\n\n  if(isBinary(parent) && parent.operation === \"&&\" && parent.left === ast){\n    return true;\n  }\n\n  return isInsideConditionalExpressionCondition(ctx, parent);\n}\n\nfunction getStringConcatenationMeta(ctx: Rule.RuleContext, ast: AST, isConcatenatedLeft = false, isConcatenatedRight = false): { isConcatenatedLeft: boolean; isConcatenatedRight: boolean; } {\n  const parent = findParent(ctx, ast);\n  if(!parent){\n    return {\n      isConcatenatedLeft,\n      isConcatenatedRight\n    };\n  }\n\n  if(isBinary(parent) && parent.operation === \"+\"){\n    return getStringConcatenationMeta(\n      ctx,\n      parent,\n      isConcatenatedLeft || parent.right === ast,\n      isConcatenatedRight || parent.left === ast\n    );\n  }\n\n  return getStringConcatenationMeta(ctx, parent, isConcatenatedLeft, isConcatenatedRight);\n}\n\nfunction isInsideObjectValue(ctx: Rule.RuleContext, ast: AST): boolean {\n  const parent = findParent(ctx, ast);\n  if(!parent){ return false; }\n\n  // #34 allow call expressions as object values\n  if(isCallExpression(ast)){ return false; }\n\n  if(isObjectValue(ast)){\n    return true;\n  }\n\n  if(isLiteralMap(parent) && parent.values.includes(ast)){\n    return true;\n  }\n\n  return isInsideObjectValue(ctx, parent);\n}\n\nfunction isStringLike(ast: AST): ast is LiteralPrimitive | TemplateLiteralElement {\n  return isStringLiteral(ast) || isTemplateLiteralElement(ast);\n}\n\nfunction hasParent(ast: AST): ast is AST & Parent {\n  return \"parent\" in ast && ast.parent !== undefined;\n}\n\n/**\n * The angular parser doesn't provide parent references for all nodes. This function traverses the entire AST\n * to find the parent node of the given AST reference.\n *\n * @param ctx The ESLint rule context.\n * @param astNode The AST node to find the parent for.\n * @returns The parent AST node, or undefined if not found.\n */\nfunction findParent(ctx: Rule.RuleContext, astNode: AST): AST | undefined {\n  if(hasParent(astNode)){\n    return astNode.parent;\n  }\n\n  const ast = ctx.sourceCode.ast;\n\n  const visitChildNode = (childNode: unknown) => {\n    if(!childNode || typeof childNode !== \"object\"){\n      return;\n    }\n\n    for(const key in childNode){\n      if(key === \"parent\"){\n        continue;\n      }\n\n      if(childNode[key] === astNode){\n        return childNode;\n      }\n\n      const result = visitChildNode(childNode[key]);\n\n      if(result){\n        return result;\n      }\n    }\n  };\n\n  return visitChildNode(ast);\n}\n\nfunction isBoundAttributeName(ast: AST | TmplAstBoundAttribute | TmplAstTextAttribute): boolean {\n  return isBoundAttribute(ast) && getAttributeName(ast)?.startsWith(\"[class.\");\n}\n\nfunction isObjectValue(ast: AST): ast is LiteralPrimitive {\n  return isStringLiteral(ast) && hasParent(ast) && isLiteralMap(ast.parent);\n}\n\nfunction isObjectKey(ast: Record<string, any>): ast is LiteralMapPropertyKey {\n  return \"type\" in ast && ast.type === \"Object\" && \"key\" in ast && ast.key !== undefined;\n}\n\nfunction isStringLiteral(ast: AST): ast is LiteralPrimitive {\n  return isLiteralPrimitive(ast) && typeof ast.value === \"string\";\n}\n\nexport function isAST(ast: unknown): ast is AST {\n  return typeof ast === \"object\" && ast !== null && \"type\" in ast;\n}\n\nfunction is<Type extends AST | TmplAstNode>(ast: AST | TmplAstNode, type: string): ast is Type {\n  return \"type\" in ast && typeof ast.type === \"string\" && ast.type === type;\n}\n\nconst isCallExpression = (ast: AST) => is<Call>(ast, \"Call\");\nconst isASTWithSource = (ast: AST) => is<ASTWithSource>(ast, \"ASTWithSource\");\nconst isInterpolation = (ast: AST) => is<Interpolation>(ast, \"Interpolation\");\nconst isConditional = (ast: AST) => is<Conditional>(ast, \"Conditional\");\nconst isBinary = (ast: AST) => is<Binary>(ast, \"Binary\");\nconst isLiteralArray = (ast: AST) => is<LiteralArray>(ast, \"LiteralArray\");\nconst isLiteralMap = (ast: AST) => is<LiteralMap>(ast, \"LiteralMap\");\nconst isTemplateLiteral = (ast: AST) => is<TemplateLiteral>(ast, \"TemplateLiteral\");\nconst isTemplateLiteralElement = (ast: AST) => is<TemplateLiteralElement>(ast, \"TemplateLiteralElement\");\nconst isLiteralPrimitive = (ast: AST) => is<LiteralPrimitive>(ast, \"LiteralPrimitive\");\n\nconst isTextAttribute = (ast: AST | TmplAstBoundAttribute | TmplAstTextAttribute) => is<TmplAstTextAttribute>(ast, \"TextAttribute\");\nconst isBoundAttribute = (ast: AST | TmplAstBoundAttribute | TmplAstTextAttribute) => is<TmplAstBoundAttribute>(ast, \"BoundAttribute\");\n"
  },
  {
    "path": "src/parsers/css.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentLineWrapping } from \"better-tailwindcss:rules/enforce-consistent-line-wrapping.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"css\", () => {\n  it(\"should lint single classes\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          css: `@utility test { @apply  lint ; }`,\n          cssOutput: `@utility test { @apply lint; }`,\n\n          errors: 2\n        }\n      ]\n    });\n  });\n\n  it(\"should lint multiple classes\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          css: `@utility test { @apply  lint  lint ; }`,\n          cssOutput: `@utility test { @apply lint lint; }`,\n\n          errors: 3\n        }\n      ]\n    });\n  });\n\n  it(\"should lint multiline class lists\", () => {\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          css: `\n            @utility test {\n              @apply lint hover:lint ;\n            }\n          `,\n          cssOutput: `\n            @utility test {\n              @apply \n                lint\n                hover:lint\n              ;\n            }\n          `,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should support raw prelude\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          css: `@utility test { @apply  text-[red] ; }`,\n          cssOutput: `@utility test { @apply text-[red]; }`,\n\n          errors: 2\n        }\n      ]\n    });\n  });\n\n  it(\"should add a whitespace when collapsing to a single line\", () => {\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          css: `\n            @utility test {\n              @apply\n                text-red-500\n              ;\n            }\n          `,\n          cssOutput: `\n            @utility test {\n              @apply text-red-500;\n            }\n          `,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/parsers/css.ts",
    "content": "import { getIndentation, getWhitespace } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Atrule } from \"@eslint/css-tree\";\nimport type { Rule } from \"eslint\";\n\nimport type { CSSClassListLiteral, Literal, Loc, Range } from \"better-tailwindcss:types/ast.js\";\n\n\nexport function getLiteralsByCSSAtRule(ctx: Rule.RuleContext, node: Atrule): Literal[] {\n\n  const literals: Literal[] = [];\n\n  if(node.name !== \"apply\"){ return []; }\n\n  if(node.prelude?.type === \"AtrulePrelude\" || node.prelude?.type === \"Raw\"){\n    const literal = getLiteralsByAtrule(ctx, node);\n    if(literal){\n      literals.push(literal);\n    }\n  }\n\n  return literals;\n\n}\n\nfunction getLiteralsByAtrule(ctx: Rule.RuleContext, node: Atrule): CSSClassListLiteral | undefined {\n\n  // @ts-expect-error - CSS Tree types are different\n  const raw = ctx.sourceCode.getText(node);\n\n  const match = raw.match(/^(?<leadingApply>@apply[\\t ](?!\\r?\\n)|@apply(?=\\s))(?<content>.+?)(?<trailingSemicolon>;?\\s*)$/s);\n\n  if(!match?.groups?.leadingApply || !match.groups.content || match.groups.trailingSemicolon === undefined){\n    return;\n  }\n\n  const { content, leadingApply, trailingSemicolon } = match.groups;\n\n  const startOffset = leadingApply.length;\n  const endOffset = trailingSemicolon.length;\n\n  const loc = getLoc(ctx, node, startOffset, endOffset);\n  const range = getRange(ctx, node, startOffset, endOffset);\n\n  if(!loc){\n    return;\n  }\n\n  const line = ctx.sourceCode.lines[node.loc!.start.line - 1];\n  const indentation = getIndentation(line);\n  const whitespaces = getWhitespace(content);\n  const type = \"CSSClassListLiteral\";\n\n  return {\n    ...whitespaces,\n    content,\n    indentation,\n    isInterpolated: false,\n    leadingApply,\n    loc,\n    range,\n    raw: content,\n    supportsMultiline: true,\n    trailingSemicolon,\n    type\n  };\n\n}\n\nfunction getLoc(ctx: Rule.RuleContext, node: Atrule, startOffset: number, endOffset: number): Loc[\"loc\"] | undefined {\n  if(!node.loc){\n    return;\n  }\n\n  return {\n    end: {\n      column: node.loc.end.column - endOffset,\n      line: node.loc.end.line\n    },\n    start: {\n      column: node.loc.start.column + startOffset,\n      line: node.loc.start.line\n    }\n  };\n}\n\nfunction getRange(ctx: Rule.RuleContext, node: Atrule, startOffset: number, endOffset: number): Range[\"range\"] {\n  const range = ctx.sourceCode\n    // @ts-expect-error - CSS Tree types are different\n    .getRange(node);\n\n  return [\n    range[0] + startOffset,\n    range[1] - endOffset\n  ];\n}\n"
  },
  {
    "path": "src/parsers/es.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"es\", () => {\n\n  it(\"should match callees names via regex\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" lint \");`,\n          jsxOutput: `testStyles(\"lint\");`,\n          svelte: `<script>testStyles(\" lint \");</script>`,\n          svelteOutput: `<script>testStyles(\"lint\");</script>`,\n          vue: `<script>testStyles(\" lint \");</script>`,\n          vueOutput: `<script>testStyles(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            callees: [\"^.*Styles$\"]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support callee target last for curried calls\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" keep \")(\" lint \");`,\n          jsxOutput: `testStyles(\" keep \")(\"lint\");`,\n          svelte: `<script>testStyles(\" keep \")(\" lint \");</script>`,\n          svelteOutput: `<script>testStyles(\" keep \")(\"lint\");</script>`,\n          vue: `<script>testStyles(\" keep \")(\" lint \");</script>`,\n          vueOutput: `<script>testStyles(\" keep \")(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                callTarget: \"last\",\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support callee target all for curried calls\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" first \")(\" second \");`,\n          jsxOutput: `testStyles(\"first\")(\"second\");`,\n          svelte: `<script>testStyles(\" first \")(\" second \");</script>`,\n          svelteOutput: `<script>testStyles(\"first\")(\"second\");</script>`,\n          vue: `<script>testStyles(\" first \")(\" second \");</script>`,\n          vueOutput: `<script>testStyles(\"first\")(\"second\");</script>`,\n\n          errors: 4,\n          options: [{\n            selectors: [\n              {\n                callTarget: \"all\",\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support numeric and negative callee targets\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" keep \")(\" middle \")(\" lint \");`,\n          jsxOutput: `testStyles(\" keep \")(\" middle \")(\"lint\");`,\n          svelte: `<script>testStyles(\" keep \")(\" middle \")(\" lint \");</script>`,\n          svelteOutput: `<script>testStyles(\" keep \")(\" middle \")(\"lint\");</script>`,\n          vue: `<script>testStyles(\" keep \")(\" middle \")(\" lint \");</script>`,\n          vueOutput: `<script>testStyles(\" keep \")(\" middle \")(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                callTarget: -1,\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\"\n              }\n            ]\n          }]\n        }\n      ],\n      valid: [\n        {\n          jsx: `testStyles(\" keep \")(\" middle \")(\" lint \");`,\n          svelte: `<script>testStyles(\" keep \")(\" middle \")(\" lint \");</script>`,\n          vue: `<script>testStyles(\" keep \")(\" middle \")(\" lint \");</script>`,\n\n          options: [{\n            selectors: [\n              {\n                callTarget: 5,\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support targetCall for curried calls\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" keep \")(\" lint \");`,\n          jsxOutput: `testStyles(\" keep \")(\"lint\");`,\n          svelte: `<script>testStyles(\" keep \")(\" lint \");</script>`,\n          svelteOutput: `<script>testStyles(\" keep \")(\"lint\");</script>`,\n          vue: `<script>testStyles(\" keep \")(\" lint \");</script>`,\n          vueOutput: `<script>testStyles(\" keep \")(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetCall: \"last\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint all targetArguments by default\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" lint \", \" lint \");`,\n          jsxOutput: `testStyles(\"lint\", \"lint\");`,\n          svelte: `<script>testStyles(\" lint \", \" lint \");</script>`,\n          svelteOutput: `<script>testStyles(\"lint\", \"lint\");</script>`,\n          vue: `<script>testStyles(\" lint \", \" lint \");</script>`,\n          vueOutput: `<script>testStyles(\"lint\", \"lint\");</script>`,\n\n          errors: 4,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support targetArgument for direct callee arguments\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" lint \", \" keep \");`,\n          jsxOutput: `testStyles(\"lint\", \" keep \");`,\n          svelte: `<script>testStyles(\" lint \", \" keep \");</script>`,\n          svelteOutput: `<script>testStyles(\"lint\", \" keep \");</script>`,\n          vue: `<script>testStyles(\" lint \", \" keep \");</script>`,\n          vueOutput: `<script>testStyles(\"lint\", \" keep \");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: \"first\"\n              }\n            ]\n          }]\n        },\n        {\n          jsx: `testStyles(\" keep \", \" lint \");`,\n          jsxOutput: `testStyles(\" keep \", \"lint\");`,\n          svelte: `<script>testStyles(\" keep \", \" lint \");</script>`,\n          svelteOutput: `<script>testStyles(\" keep \", \"lint\");</script>`,\n          vue: `<script>testStyles(\" keep \", \" lint \");</script>`,\n          vueOutput: `<script>testStyles(\" keep \", \"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: 1\n              }\n            ]\n          }]\n        },\n        {\n          jsx: `testStyles(...foo, \" lint \");`,\n          jsxOutput: `testStyles(...foo, \"lint\");`,\n          svelte: `<script>testStyles(...foo, \" lint \");</script>`,\n          svelteOutput: `<script>testStyles(...foo, \"lint\");</script>`,\n          vue: `<script>testStyles(...foo, \" lint \");</script>`,\n          vueOutput: `<script>testStyles(...foo, \"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: 1\n              }\n            ]\n          }]\n        }\n      ],\n      valid: [\n        {\n          jsx: `testStyles(\" keep \", \" keep \");`,\n          svelte: `<script>testStyles(\" keep \", \" keep \");</script>`,\n          vue: `<script>testStyles(\" keep \", \" keep \");</script>`,\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: 5\n              }\n            ]\n          }]\n        },\n        {\n          jsx: `testStyles(...foo, \" keep \");`,\n          svelte: `<script>testStyles(...foo, \" keep \");</script>`,\n          vue: `<script>testStyles(...foo, \" keep \");</script>`,\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: 0\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should apply first and last targetArgument to raw argument positions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" lint \", ...foo);`,\n          jsxOutput: `testStyles(\"lint\", ...foo);`,\n          svelte: `<script>testStyles(\" lint \", ...foo);</script>`,\n          svelteOutput: `<script>testStyles(\"lint\", ...foo);</script>`,\n          vue: `<script>testStyles(\" lint \", ...foo);</script>`,\n          vueOutput: `<script>testStyles(\"lint\", ...foo);</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: \"first\"\n              }\n            ]\n          }]\n        }\n      ],\n      valid: [\n        {\n          jsx: `testStyles(...foo, \" keep \");`,\n          svelte: `<script>testStyles(...foo, \" keep \");</script>`,\n          vue: `<script>testStyles(...foo, \" keep \");</script>`,\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: \"first\"\n              }\n            ]\n          }]\n        },\n        {\n          jsx: `testStyles(\" keep \", ...foo);`,\n          svelte: `<script>testStyles(\" keep \", ...foo);</script>`,\n          vue: `<script>testStyles(\" keep \", ...foo);</script>`,\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: \"last\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support combining targetCall and targetArgument\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(\" keep \", \" ignore \")(\" lint \", \" keep \");`,\n          jsxOutput: `testStyles(\" keep \", \" ignore \")(\"lint\", \" keep \");`,\n          svelte: `<script>testStyles(\" keep \", \" ignore \")(\" lint \", \" keep \");</script>`,\n          svelteOutput: `<script>testStyles(\" keep \", \" ignore \")(\"lint\", \" keep \");</script>`,\n          vue: `<script>testStyles(\" keep \", \" ignore \")(\" lint \", \" keep \");</script>`,\n          vueOutput: `<script>testStyles(\" keep \", \" ignore \")(\"lint\", \" keep \");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                name: \"^testStyles$\",\n                targetArgument: \"first\",\n                targetCall: \"last\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should apply matchers only inside selected arguments\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(...[{ objectKey: \" lint \" }], \" keep \");`,\n          jsxOutput: `testStyles(...[{ objectKey: \"lint\" }], \" keep \");`,\n          svelte: `<script>testStyles(...[{ objectKey: \" lint \" }], \" keep \");</script>`,\n          svelteOutput: `<script>testStyles(...[{ objectKey: \"lint\" }], \" keep \");</script>`,\n          vue: `<script>testStyles(...[{ objectKey: \" lint \" }], \" keep \");</script>`,\n          vueOutput: `<script>testStyles(...[{ objectKey: \"lint\" }], \" keep \");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                match: [{ type: MatcherType.ObjectValue }],\n                name: \"^testStyles$\",\n                targetArgument: 0\n              }\n            ]\n          }]\n        }\n      ],\n      valid: [\n        {\n          jsx: `testStyles({ objectKey: \" keep \" }, \" keep \");`,\n          svelte: `<script>testStyles({ objectKey: \" keep \" }, \" keep \");</script>`,\n          vue: `<script>testStyles({ objectKey: \" keep \" }, \" keep \");</script>`,\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                match: [{ type: MatcherType.ObjectValue }],\n                name: \"^testStyles$\",\n                targetArgument: \"last\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match anonymous arrow function returns\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(() => \" lint \", () => { return \" lint \"; });`,\n          jsxOutput: `testStyles(() => \"lint\", () => { return \"lint\"; });`,\n          svelte: `<script>testStyles(() => \" lint \", () => { return \" lint \"; });</script>`,\n          svelteOutput: `<script>testStyles(() => \"lint\", () => { return \"lint\"; });</script>`,\n          vue: `<script>testStyles(() => \" lint \", () => { return \" lint \"; });</script>`,\n          vueOutput: `<script>testStyles(() => \"lint\", () => { return \"lint\"; });</script>`,\n\n          errors: 4,\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [{ type: MatcherType.String }],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should only match concise arrow returned expression\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles((param = \" keep \") => \" lint \");`,\n          jsxOutput: `testStyles((param = \" keep \") => \"lint\");`,\n          svelte: `<script>testStyles((param = \" keep \") => \" lint \");</script>`,\n          svelteOutput: `<script>testStyles((param = \" keep \") => \"lint\");</script>`,\n          vue: `<script>testStyles((param = \" keep \") => \" lint \");</script>`,\n          vueOutput: `<script>testStyles((param = \" keep \") => \"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [{ type: MatcherType.String }],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not match non-return literals inside anonymous arrow function block bodies\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `testStyles(() => { const value = \" keep \"; return value; });`,\n          svelte: `<script>testStyles(() => { const value = \" keep \"; return value; });</script>`,\n          vue: `<script>testStyles(() => { const value = \" keep \"; return value; });</script>`,\n\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [{ type: MatcherType.String }],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match anonymous normal function returns\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(function() { return \" lint \"; });`,\n          jsxOutput: `testStyles(function() { return \"lint\"; });`,\n          svelte: `<script>testStyles(function() { return \" lint \"; });</script>`,\n          svelteOutput: `<script>testStyles(function() { return \"lint\"; });</script>`,\n          vue: `<script>testStyles(function() { return \" lint \"; });</script>`,\n          vueOutput: `<script>testStyles(function() { return \"lint\"; });</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [{ type: MatcherType.String }],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should support all other nested matcher types inside anonymousFunctionReturn\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(() => ({ \" objectKey \": \" objectValue \" }));`,\n          jsxOutput: `testStyles(() => ({ \"objectKey\": \"objectValue\" }));`,\n          svelte: `<script>testStyles(() => ({ \" objectKey \": \" objectValue \" }));</script>`,\n          svelteOutput: `<script>testStyles(() => ({ \"objectKey\": \"objectValue\" }));</script>`,\n          vue: `<script>testStyles(() => ({ \" objectKey \": \" objectValue \" }));</script>`,\n          vueOutput: `<script>testStyles(() => ({ \"objectKey\": \"objectValue\" }));</script>`,\n\n          errors: 4,\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [\n                  { type: MatcherType.ObjectKey },\n                  { type: MatcherType.ObjectValue }\n                ],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        },\n        {\n          jsx: `testStyles(() => \" string \");`,\n          jsxOutput: `testStyles(() => \"string\");`,\n          svelte: `<script>testStyles(() => \" string \");</script>`,\n          svelteOutput: `<script>testStyles(() => \"string\");</script>`,\n          vue: `<script>testStyles(() => \" string \");</script>`,\n          vueOutput: `<script>testStyles(() => \"string\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [\n                  { type: MatcherType.String }\n                ],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not cross function boundary twice for anonymousFunctionReturn\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `testStyles(() => { setTimeout(() => { return \" keep \"; }); return \" lint \"; });`,\n          jsxOutput: `testStyles(() => { setTimeout(() => { return \" keep \"; }); return \"lint\"; });`,\n          svelte: `<script>testStyles(() => { setTimeout(() => { return \" keep \"; }); return \" lint \"; });</script>`,\n          svelteOutput: `<script>testStyles(() => { setTimeout(() => { return \" keep \"; }); return \"lint\"; });</script>`,\n          vue: `<script>testStyles(() => { setTimeout(() => { return \" keep \"; }); return \" lint \"; });</script>`,\n          vueOutput: `<script>testStyles(() => { setTimeout(() => { return \" keep \"; }); return \"lint\"; });</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Callee,\n              match: [{\n                match: [\n                  { type: MatcherType.String }\n                ],\n                type: MatcherType.AnonymousFunctionReturn\n              }],\n              name: \"^testStyles$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match member expression callee names\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `const classes = []; classes.push(\" lint \");`,\n          jsxOutput: `const classes = []; classes.push(\"lint\");`,\n          svelte: `<script>const classes = []; classes.push(\" lint \");</script>`,\n          svelteOutput: `<script>const classes = []; classes.push(\"lint\");</script>`,\n          vue: `<script>const classes = []; classes.push(\" lint \");</script>`,\n          vueOutput: `<script>const classes = []; classes.push(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            callees: [[\n              \"^classes\\\\.push$\",\n              [{ match: MatcherType.String }]\n            ]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match nested member expression callee names\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `const foo = { bar: { push: (value) => value } }; foo.bar.push(\" lint \");`,\n          jsxOutput: `const foo = { bar: { push: (value) => value } }; foo.bar.push(\"lint\");`,\n          svelte: `<script>const foo = { bar: { push: (value) => value } }; foo.bar.push(\" lint \");</script>`,\n          svelteOutput: `<script>const foo = { bar: { push: (value) => value } }; foo.bar.push(\"lint\");</script>`,\n          vue: `<script>const foo = { bar: { push: (value) => value } }; foo.bar.push(\" lint \");</script>`,\n          vueOutput: `<script>const foo = { bar: { push: (value) => value } }; foo.bar.push(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            callees: [[\n              \"^foo\\\\.bar\\\\.push$\",\n              [{ match: MatcherType.String }]\n            ]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match callee selectors via path\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `const classes = [\"keep\"]; classes.push(\" lint \");`,\n          jsxOutput: `const classes = [\"keep\"]; classes.push(\"lint\");`,\n          svelte: `<script>const classes = [\"keep\"]; classes.push(\" lint \");</script>`,\n          svelteOutput: `<script>const classes = [\"keep\"]; classes.push(\"lint\");</script>`,\n          vue: `<script>const classes = [\"keep\"]; classes.push(\" lint \");</script>`,\n          vueOutput: `<script>const classes = [\"keep\"]; classes.push(\"lint\");</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Callee,\n                match: [{ type: MatcherType.String }],\n                path: \"^classes\\\\.push$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match variable names via regex\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `const testStyles = \" lint \";`,\n          jsxOutput: `const testStyles = \"lint\";`,\n          svelte: `<script>const testStyles = \" lint \";</script>`,\n          svelteOutput: `<script>const testStyles = \"lint\";</script>`,\n          vue: `<script>const testStyles = \" lint \";</script>`,\n          vueOutput: `<script>const testStyles = \"lint\";</script>`,\n\n          errors: 2,\n          options: [{\n            variables: [\"^.*Styles$\"]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match default exports via variable selectors\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `export default \" lint \";`,\n          jsxOutput: `export default \"lint\";`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Variable,\n                name: \"^default$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not dereference exported default identifiers for variable selectors\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `const classes = \" lint \"; export default classes;`,\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Variable,\n                name: \"^default$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match default-exported objects\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `export default { slots: { root: \" lint \", icon: \" keep \" }, title: \" keep \" };`,\n          jsxOutput: `export default { slots: { root: \"lint\", icon: \" keep \" }, title: \" keep \" };`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Variable,\n                match: [\n                  {\n                    path: \"^slots\\\\.root$\",\n                    type: MatcherType.ObjectValue\n                  }\n                ],\n                name: \"^default$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match attributes via regex\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `<img testStyles=\" lint \" />`,\n          jsxOutput: `<img testStyles=\"lint\" />`,\n          svelte: `<img testStyles=\" lint \" />`,\n          svelteOutput: `<img testStyles=\"lint\" />`,\n          vue: `<template><img testStyles=\" lint \" /> </template>`,\n          vueOutput: `<template><img testStyles=\"lint\" /> </template>`,\n\n          errors: 2,\n          options: [{\n            attributes: [\"^.*Styles$\"]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #234\n  it(\"should ignore literals in binary comparisons\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `<img class={{ \"bg-primary\": category === \" members \" }} />`,\n          svelte: `<img class={{ \"bg-primary\": category === \" members \" }} />`,\n          vue: `<template><img :class=\"{ 'bg-primary': category === ' members ' }\" /></template>`,\n\n          options: [{\n            attributes: [[\n              \"^v-bind:class$\",\n              [{ match: MatcherType.ObjectValue }]\n            ], [\n              \"class\",\n              [{ match: MatcherType.ObjectValue }]\n            ]]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #332\n  it(\"should not leak variable selectors into callee selectors when assigned to a variable\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `const variable = function func({ classes = \" sm \" }) {}`,\n\n          options: [{\n            selectors: [{\n              kind: SelectorKind.Variable,\n              match: [\n                {\n                  type: MatcherType.String\n                }\n              ],\n              name: \"^variable$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint bare template literals with matching marker comments\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"/* tw */` lint `;\",\n          jsxOutput: \"/* tw */`lint`;\",\n          svelte: \"<script>/* tw */` lint `;</script>\",\n          svelteOutput: \"<script>/* tw */`lint`;</script>\",\n          vue: \"<script>/* tw */` lint `;</script>\",\n          vueOutput: \"<script>/* tw */`lint`;</script>\",\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Tag,\n                name: \"^tw$\"\n              }\n            ]\n          }]\n        },\n        {\n          jsx: \"/* tw */ ` lint `;\",\n          jsxOutput: \"/* tw */ `lint`;\",\n          svelte: \"<script>/* tw */ ` lint `;</script>\",\n          svelteOutput: \"<script>/* tw */ `lint`;</script>\",\n          vue: \"<script>/* tw */ ` lint `;</script>\",\n          vueOutput: \"<script>/* tw */ `lint`;</script>\",\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Tag,\n                name: \"^tw$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should ignore bare template literals when marker comments do not match or are not leading\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: \"/* tw */ const keep = true; ` lint `;\",\n          svelte: \"<script>/* tw */ const keep = true; ` lint `;</script>\",\n          vue: \"<script>/* tw */ const keep = true; ` lint `;</script>\",\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Tag,\n                name: \"^tw$\"\n              }\n            ]\n          }]\n        },\n        {\n          jsx: \"/* not-tailwind */ ` lint `;\",\n          svelte: \"<script>/* not-tailwind */ ` lint `;</script>\",\n          vue: \"<script>/* not-tailwind */ ` lint `;</script>\",\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Tag,\n                name: \"^tw$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should only use the closest leading marker comment for bare template literals\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"/* not-tailwind */ /* tw */ ` lint `;\",\n          jsxOutput: \"/* not-tailwind */ /* tw */ `lint`;\",\n          svelte: \"<script>/* not-tailwind */ /* tw */ ` lint `;</script>\",\n          svelteOutput: \"<script>/* not-tailwind */ /* tw */ `lint`;</script>\",\n          vue: \"<script>/* not-tailwind */ /* tw */ ` lint `;</script>\",\n          vueOutput: \"<script>/* not-tailwind */ /* tw */ `lint`;</script>\",\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Tag,\n                name: \"^tw$\"\n              }\n            ]\n          }]\n        }\n      ],\n      valid: [\n        {\n          jsx: \"/* tw */ /* not-tailwind */ ` lint `;\",\n          svelte: \"<script>/* tw */ /* not-tailwind */ ` lint `;</script>\",\n          vue: \"<script>/* tw */ /* not-tailwind */ ` lint `;</script>\",\n\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Tag,\n                name: \"^tw$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/parsers/es.ts",
    "content": "import { MATCHER_RESULT, MatcherType } from \"better-tailwindcss:types/rule.js\";\nimport {\n  getLiteralNodesByMatchers,\n  isIndexedAccessLiteral,\n  isInsideConditionalExpressionTest,\n  isInsideDisallowedBinaryExpression,\n  isInsideLogicalExpressionLeft,\n  isInsideMemberExpression,\n  matchesPathPattern\n} from \"better-tailwindcss:utils/matchers.js\";\nimport {\n  createObjectPathElement,\n  deduplicateLiterals,\n  getContent,\n  getIndentation,\n  getQuotes,\n  getWhitespace,\n  isGenericNodeWithParent,\n  matchesName\n} from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Rule } from \"eslint\";\nimport type {\n  ArrowFunctionExpression as ESArrowFunctionExpression,\n  BaseNode as ESBaseNode,\n  CallExpression as ESCallExpression,\n  ExportDefaultDeclaration as ESExportDefaultDeclaration,\n  Expression as ESExpression,\n  FunctionDeclaration as ESFunctionDeclaration,\n  FunctionExpression as ESFunctionExpression,\n  Identifier as ESIdentifier,\n  MemberExpression as ESMemberExpression,\n  Node as ESNode,\n  SimpleLiteral as ESSimpleLiteral,\n  SpreadElement as ESSpreadElement,\n  TaggedTemplateExpression as ESTaggedTemplateExpression,\n  TemplateElement as ESTemplateElement,\n  TemplateLiteral as ESTemplateLiteral,\n  VariableDeclarator as ESVariableDeclarator\n} from \"estree\";\n\nimport type {\n  BracesMeta,\n  Literal,\n  LiteralValueQuotes,\n  MultilineMeta,\n  StringLiteral,\n  TemplateLiteral\n} from \"better-tailwindcss:types/ast.js\";\nimport type { WithParent } from \"better-tailwindcss:types/estree.js\";\nimport type {\n  ArgumentTarget,\n  CalleeSelector,\n  CallTarget,\n  MatcherFunctions,\n  SelectorMatcher,\n  TagSelector,\n  VariableSelector\n} from \"better-tailwindcss:types/rule.js\";\nimport type { GenericNodeWithParent } from \"better-tailwindcss:utils/utils.js\";\n\n\nexport const ES_CONTAINER_TYPES_TO_REPLACE_QUOTES: string[] = [\n  \"ArrayExpression\",\n  \"Property\",\n  \"CallExpression\",\n  \"VariableDeclarator\",\n  \"ConditionalExpression\",\n  \"LogicalExpression\"\n];\n\nexport const ES_CONTAINER_TYPES_TO_INSERT_BRACES: string[] = [\n];\n\n\nexport function getLiteralsByESVariableDeclarator(ctx: Rule.RuleContext, node: ESVariableDeclarator, selectors: VariableSelector[]): Literal[] {\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n\n    if(!node.init){ return literals; }\n    if(!isESVariableSymbol(node.id)){ return literals; }\n    if(!matchesName(selector.name, node.id.name)){ return literals; }\n\n    if(!selector.match){\n      literals.push(...getLiteralsByESExpression(ctx, [node.init]));\n      return literals;\n    }\n\n    if(isESArrowFunctionExpression(node.init) || isESCallExpression(node.init) || isESFunctionExpression(node.init)){\n      return literals;\n    }\n\n    literals.push(...getLiteralsByESMatchers(ctx, node.init, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals.filter(deduplicateLiterals);\n\n}\n\nexport function getLiteralsByESExportDefaultDeclaration(ctx: Rule.RuleContext, node: ESExportDefaultDeclaration, selectors: VariableSelector[]): Literal[] {\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n\n    if(!matchesName(selector.name, \"default\")){ return literals; }\n    if(!isESExportDefaultExpression(node.declaration)){ return literals; }\n\n    if(!selector.match){\n      literals.push(...getLiteralsByESExpression(ctx, [node.declaration]));\n      return literals;\n    }\n\n    if(isESArrowFunctionExpression(node.declaration) || isESCallExpression(node.declaration) || isESFunctionExpression(node.declaration)){\n      return literals;\n    }\n\n    literals.push(...getLiteralsByESMatchers(ctx, node.declaration, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals.filter(deduplicateLiterals);\n\n}\n\nexport function getLiteralsByESCallExpression(ctx: Rule.RuleContext, node: ESCallExpression, selectors: CalleeSelector[]): Literal[] {\n\n  if(isNestedCurriedCall(node)){\n    return [];\n  }\n\n  const callChain = getCurriedCallChain(node);\n\n  if(!callChain){ return []; }\n\n  const calleePath = getESCalleeName(callChain[0].callee, \"path\");\n  const calleeName = getESCalleeName(callChain[0].callee, \"name\");\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n\n    if(\n      !selector.path && !selector.name ||\n      (!selector.path || !matchesName(selector.path, calleePath)) &&\n      (!selector.name || !matchesName(selector.name, calleeName))\n    ){\n      return literals;\n    }\n\n    const targetCall = selector.targetCall ?? selector.callTarget;\n    const targetCalls = getTargetCalls(callChain, targetCall);\n\n    for(const targetCall of targetCalls){\n      const targetArguments = getTargetArguments(targetCall.arguments, selector.targetArgument);\n\n      if(!selector.match){\n        literals.push(...getLiteralsByESExpression(ctx, targetArguments));\n        continue;\n      }\n\n      for(const targetArgument of targetArguments){\n        literals.push(...getLiteralsByESMatchers(ctx, targetArgument, selector.match));\n      }\n    }\n\n    return literals;\n  }, []);\n\n  return literals.filter(deduplicateLiterals);\n\n}\n\nexport function getLiteralsByTaggedTemplateExpression(ctx: Rule.RuleContext, node: ESTaggedTemplateExpression, selectors: TagSelector[]): Literal[] {\n  const tagPath = getTaggedTemplateName(node.tag, \"path\");\n  const tagName = getTaggedTemplateName(node.tag, \"name\");\n\n  if(!tagPath && !tagName){ return []; }\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n\n    if(\n      !selector.path && !selector.name ||\n      (!selector.path || !matchesName(selector.path, tagPath)) &&\n      (!selector.name || !matchesName(selector.name, tagName))\n    ){\n      return literals;\n    }\n\n    if(!selector.match){\n      literals.push(...getLiteralsByESTemplateLiteral(ctx, node.quasi));\n      return literals;\n    }\n\n    literals.push(...getLiteralsByESMatchers(ctx, node, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals.filter(deduplicateLiterals);\n\n}\n\nexport function getLiteralsByESBareTemplateLiteral(ctx: Rule.RuleContext, node: ESTemplateLiteral, selectors: TagSelector[]): Literal[] {\n  const leadingComment = getLeadingComment(ctx, node);\n\n  if(isTaggedTemplateLiteral(node) || !leadingComment){ return [];}\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n    if(!selector.name || !matchesName(selector.name, leadingComment)){ return literals; }\n\n    if(!selector.match){\n      literals.push(...getLiteralsByESTemplateLiteral(ctx, node));\n      return literals;\n    }\n\n    literals.push(...getLiteralsByESMatchers(ctx, node, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals.filter(deduplicateLiterals);\n}\n\nexport function getLiteralsByESLiteralNode(ctx: Rule.RuleContext, node: ESBaseNode): Literal[] {\n\n  if(isESSimpleStringLiteral(node)){\n    const literal = getStringLiteralByESStringLiteral(ctx, node);\n    return literal ? [literal] : [];\n  }\n\n  if(isESTemplateLiteral(node)){\n    return getLiteralsByESTemplateLiteral(ctx, node);\n  }\n\n  if(isESTemplateElement(node) && hasESNodeParentExtension(node)){\n    const literal = getLiteralByESTemplateElement(ctx, node);\n    return literal ? [literal] : [];\n  }\n\n  return [];\n\n}\n\n\nexport function getLiteralsByESMatchers(ctx: Rule.RuleContext, node: ESBaseNode, matchers: SelectorMatcher[]): Literal[] {\n  const matcherFunctions = getESMatcherFunctions(matchers);\n  const literalNodes = getLiteralNodesByMatchers<ESBaseNode>(ctx, node, matcherFunctions);\n  const literals = literalNodes.flatMap(literalNode => getLiteralsByESLiteralNode(ctx, literalNode));\n  return literals.filter(deduplicateLiterals);\n}\n\n\nexport function getStringLiteralByESStringLiteral(ctx: Rule.RuleContext, node: ESSimpleStringLiteral): StringLiteral | undefined {\n\n  const raw = node.raw;\n\n  if(!raw || !node.loc || !node.range || !node.parent.loc || !node.parent.range){\n    return;\n  }\n\n  const line = ctx.sourceCode.lines[node.loc.start.line - 1];\n\n  const quotes = getQuotes(raw);\n  const priorLiterals = findPriorLiterals(ctx, node);\n  const content = getContent(raw, quotes);\n  const whitespaces = getWhitespace(content);\n  const indentation = getIndentation(line);\n  const multilineQuotes = getMultilineQuotes(node);\n  const supportsMultiline = !isESObjectKey(node);\n  const concatenation = getStringConcatenationMeta(node);\n\n  return {\n    ...quotes,\n    ...whitespaces,\n    ...multilineQuotes,\n    ...concatenation,\n    content,\n    indentation,\n    isInterpolated: false,\n    loc: node.loc,\n    priorLiterals,\n    range: node.range,\n    raw,\n    supportsMultiline,\n    type: \"StringLiteral\"\n  };\n\n}\n\nfunction getLiteralByESTemplateElement(ctx: Rule.RuleContext, node: ESTemplateElement & Rule.Node): TemplateLiteral | undefined {\n\n  const raw = ctx.sourceCode.getText(node);\n\n  if(!raw || !node.loc || !node.range || !node.parent.loc || !node.parent.range){\n    return;\n  }\n\n  const line = ctx.sourceCode.lines[node.parent.loc.start.line - 1];\n\n  const quotes = getQuotes(raw);\n  const braces = getBracesByString(ctx, raw);\n  const isInterpolated = getIsInterpolated(ctx, raw);\n  const priorLiterals = findPriorLiterals(ctx, node);\n  const content = getContent(raw, quotes, braces);\n  const whitespaces = getWhitespace(content);\n  const indentation = getIndentation(line);\n  const multilineQuotes = getMultilineQuotes(node);\n  const concatenation = getStringConcatenationMeta(node);\n\n  return {\n    ...whitespaces,\n    ...quotes,\n    ...braces,\n    ...multilineQuotes,\n    ...concatenation,\n    content,\n    indentation,\n    isInterpolated,\n    loc: node.loc,\n    priorLiterals,\n    range: node.range,\n    raw,\n    supportsMultiline: true,\n    type: \"TemplateLiteral\"\n  };\n\n}\n\nfunction getMultilineQuotes(node: ESNode & Rule.NodeParentExtension): MultilineMeta {\n  const surroundingBraces = ES_CONTAINER_TYPES_TO_INSERT_BRACES.includes(node.parent.type);\n  const multilineQuotes: LiteralValueQuotes[] = ES_CONTAINER_TYPES_TO_REPLACE_QUOTES.includes(node.parent.type)\n    ? [\"`\"]\n    : [];\n\n  return {\n    multilineQuotes,\n    surroundingBraces\n  };\n}\n\nfunction getLiteralsByESExpression(ctx: Rule.RuleContext, args: ESExpression[]): Literal[] {\n  return args.reduce<Literal[]>((acc, node) => {\n    acc.push(...getLiteralsByESLiteralNode(ctx, node));\n    return acc;\n  }, []);\n}\n\nexport function getLiteralsByESTemplateLiteral(ctx: Rule.RuleContext, node: ESTemplateLiteral): Literal[] {\n  return node.quasis.map(quasi => {\n    if(!hasESNodeParentExtension(quasi)){\n      return;\n    }\n    return getLiteralByESTemplateElement(ctx, quasi);\n  }).filter((literal): literal is TemplateLiteral => literal !== undefined);\n}\n\nexport function findParentESTemplateLiteralByESTemplateElement(node: WithParent<ESNode>): ESTemplateLiteral | undefined {\n  if(!hasESNodeParentExtension(node)){ return; }\n  if(node.parent.type === \"TemplateLiteral\"){ return node.parent; }\n  return findParentESTemplateLiteralByESTemplateElement(node.parent);\n}\n\nfunction findPriorLiterals(ctx: Rule.RuleContext, node: ESNode) {\n\n  if(!hasESNodeParentExtension(node)){ return; }\n\n  const priorLiterals: Literal[] = [];\n  let currentNode: ESNode = node;\n\n  while(hasESNodeParentExtension(currentNode)){\n    const parent = currentNode.parent;\n\n    if(isESCallExpression(parent)){ break; }\n    if(isESArrowFunctionExpression(parent)){ break; }\n    if(isESFunctionExpression(parent)){ break; }\n    if(isESVariableDeclarator(parent)){ break; }\n\n    if(parent.type === \"TemplateLiteral\"){\n      for(const quasi of parent.quasis){\n        if(quasi.range === node.range){\n          break;\n        }\n\n        if(quasi.type === \"TemplateElement\" && hasESNodeParentExtension(quasi)){\n          const literal = getLiteralByESTemplateElement(ctx, quasi);\n\n          if(!literal){\n            continue;\n          }\n\n          priorLiterals.push(literal);\n        }\n      }\n    }\n\n    if(parent.type === \"TemplateElement\"){\n      const literal = getLiteralByESTemplateElement(ctx, parent);\n\n      if(!literal){\n        continue;\n      }\n\n      priorLiterals.push(literal);\n    }\n\n    if(parent.type === \"Literal\"){\n      const literal = getLiteralsByESLiteralNode(ctx, parent);\n\n      if(!literal){\n        continue;\n      }\n\n      priorLiterals.push(...literal);\n    }\n\n    currentNode = parent;\n\n  }\n\n  return priorLiterals;\n\n}\n\nexport function getESObjectPath(node: WithParent<ESNode>): string | undefined {\n\n  if(!isGenericNodeWithParent(node)){ return; }\n  if(!hasESNodeParentExtension(node)){ return; }\n\n  if(\n    node.type !== \"Property\" &&\n    node.type !== \"ObjectExpression\" &&\n    node.type !== \"ArrayExpression\" &&\n    node.type !== \"Identifier\" &&\n    node.type !== \"Literal\" &&\n    node.type !== \"TemplateElement\"\n  ){\n    return;\n  }\n\n  const paths: (string | undefined)[] = [];\n\n  if(node.type === \"Property\"){\n    if(node.key.type === \"Identifier\"){\n      paths.unshift(createObjectPathElement(node.key.name));\n    } else if(node.key.type === \"Literal\"){\n      paths.unshift(createObjectPathElement(node.key.value?.toString() ?? node.key.raw));\n    } else {\n      return \"\";\n    }\n  }\n\n  if(isESStringLike(node) && isInsideObjectValue(node)){\n    const property = findMatchingParentNodes<ESNode>(node, (node): node is ESNode => {\n      return isESNode(node) && node.type === \"Property\";\n    });\n\n    if(property){\n      return getESObjectPath(property);\n    }\n  }\n\n  if(isESObjectKey(node)){\n    const property = node.parent;\n    return getESObjectPath(property);\n  }\n\n  if(node.parent.type === \"ArrayExpression\" && node.type !== \"Property\" && node.type !== \"TemplateElement\"){\n    const index = node.parent.elements.indexOf(node);\n    paths.unshift(`[${index}]`);\n  }\n\n  paths.unshift(getESObjectPath(node.parent));\n\n  return paths.reduce<string[]>((paths, currentPath) => {\n    if(!currentPath){ return paths; }\n\n    if(paths.length === 0){\n      return [currentPath];\n    }\n\n    if(currentPath.startsWith(\"[\") && currentPath.endsWith(\"]\")){\n      return [...paths, currentPath];\n    }\n\n    return [...paths, \".\", currentPath];\n  }, []).join(\"\");\n\n}\n\nexport interface ESSimpleStringLiteral extends Rule.NodeParentExtension, ESSimpleLiteral {\n  value: string;\n}\n\nexport function isESObjectKey(node: ESBaseNode & Rule.NodeParentExtension) {\n  return (\n    node.parent.type === \"Property\" &&\n    node.parent.parent.type === \"ObjectExpression\" &&\n    node.parent.key === node\n  );\n}\n\nexport function isInsideObjectValue(node: WithParent<ESNode>) {\n  if(!hasESNodeParentExtension(node)){ return false; }\n\n  // #34 allow call expressions as object values\n  if(isESCallExpression(node)){ return false; }\n  if(isESArrowFunctionExpression(node)){ return false; }\n  if(isESFunctionExpression(node)){ return false; }\n\n  if(\n    node.parent.type === \"Property\" &&\n    node.parent.parent.type === \"ObjectExpression\" &&\n    node.parent.value === node\n  ){\n    return true;\n  }\n\n  return isInsideObjectValue(node.parent);\n}\n\n\nfunction findMatchingParentNodes<Node>(node: Partial<GenericNodeWithParent>, matchesNode: (node: unknown) => node is Node): Node | undefined {\n  if(!isGenericNodeWithParent(node)){ return; }\n\n  if(matchesNode(node.parent)){\n    return node.parent as Node;\n  }\n\n  return findMatchingParentNodes(node.parent, matchesNode);\n}\n\nexport function isESSimpleStringLiteral(node: ESBaseNode): node is ESSimpleStringLiteral {\n  return (\n    node.type === \"Literal\" &&\n    \"value\" in node &&\n    typeof node.value === \"string\"\n  );\n}\n\nexport function isESStringLike(node: ESBaseNode): node is ESSimpleStringLiteral | ESTemplateElement {\n  return isESSimpleStringLiteral(node) || isESTemplateElement(node);\n}\n\nexport function isESTemplateLiteral(node: ESBaseNode): node is ESTemplateLiteral {\n  return node.type === \"TemplateLiteral\";\n}\n\nexport function isESTemplateElement(node: ESBaseNode): node is ESTemplateElement {\n  return node.type === \"TemplateElement\";\n}\n\nexport function isESNode(node: unknown): node is ESNode {\n  return (\n    node !== null &&\n    typeof node === \"object\" &&\n    \"type\" in node\n  );\n}\n\nexport function isESCallExpression(node: ESBaseNode): node is ESCallExpression {\n  return node.type === \"CallExpression\";\n}\n\nexport function isESArrowFunctionExpression(node: ESBaseNode): node is ESArrowFunctionExpression {\n  return node.type === \"ArrowFunctionExpression\";\n}\n\nexport function isESFunctionExpression(node: ESBaseNode): node is ESFunctionExpression {\n  return node.type === \"FunctionExpression\";\n}\n\nexport function isESFunctionDeclaration(node: ESBaseNode): node is ESFunctionDeclaration {\n  return node.type === \"FunctionDeclaration\";\n}\n\nexport function isESAnonymousFunction(node: ESBaseNode): boolean {\n  if(isESArrowFunctionExpression(node)){\n    return true;\n  }\n\n  if(isESFunctionExpression(node) && node.id === null){\n    return true;\n  }\n\n  return false;\n}\n\nexport function isESArrowFunctionWithoutBody(node: ESBaseNode): node is ESArrowFunctionExpression {\n  return isESArrowFunctionExpression(node) && node.body.type !== \"BlockStatement\";\n}\n\nexport function isESReturnStatement(node: ESNode): boolean {\n  return node.type === \"ReturnStatement\";\n}\n\nfunction getESMemberExpressionPropertyName(node: ESMemberExpression): string | undefined {\n  if(!node.computed && node.property.type === \"Identifier\"){\n    return node.property.name;\n  }\n\n  if(node.computed && isESSimpleStringLiteral(node.property)){\n    return node.property.value;\n  }\n}\n\nfunction getESCalleeName(node: ESBaseNode, type: \"name\" | \"path\"): string | undefined {\n  if(node.type === \"Identifier\" && \"name\" in node && typeof node.name === \"string\"){\n    return node.name;\n  }\n\n  if(node.type === \"MemberExpression\" && \"object\" in node){\n    const memberNode = node as ESMemberExpression;\n\n    if(memberNode.object.type === \"Super\"){\n      return;\n    }\n\n    const object = getESCalleeName(memberNode.object as ESBaseNode, type);\n    const property = getESMemberExpressionPropertyName(memberNode);\n\n    if(!property){\n      return;\n    }\n\n    if(type === \"name\"){\n      return property;\n    }\n\n    if(!object){\n      return;\n    }\n\n    return `${object}.${property}`;\n  }\n\n  if(node.type === \"ChainExpression\" && \"expression\" in node){\n    return getESCalleeName(node.expression as ESBaseNode, type);\n  }\n}\n\nfunction getTaggedTemplateName(node: ESBaseNode & Partial<Rule.NodeParentExtension>, type: \"name\" | \"path\"): string | undefined {\n  if(\n    node.type === \"Identifier\" && \"name\" in node && typeof node.name === \"string\" &&\n    hasESNodeParentExtension(node) &&\n    isTaggedTemplateExpression(node.parent)\n  ){\n    return node.name;\n  }\n  if(node.type === \"MemberExpression\"){\n    return getESCalleeName(node, type);\n  }\n  if(node.type === \"CallExpression\"){\n    return getESCalleeName((node as ESCallExpression).callee as ESBaseNode, type);\n  }\n  if(node.type === \"ChainExpression\" && \"expression\" in node){\n    return getTaggedTemplateName(node.expression as ESBaseNode, type);\n  }\n}\n\nfunction isNestedCurriedCall(node: ESCallExpression): boolean {\n  return hasESNodeParentExtension(node) && isESCallExpression(node.parent) && node.parent.callee === node;\n}\n\nfunction getCurriedCallChain(node: ESCallExpression) {\n  const calls: ESCallExpression[] = [node];\n  let currentCall: ESCallExpression = node;\n\n  while(isESCallExpression(currentCall.callee)){\n    currentCall = currentCall.callee;\n    calls.unshift(currentCall);\n  }\n\n  return calls;\n}\n\nfunction getTargetCalls(callChain: ESCallExpression[], callTarget: CallTarget | undefined): ESCallExpression[] {\n  return getTargetItems(callChain, callTarget, \"first\");\n}\n\nfunction getTargetArguments(args: (ESExpression | ESSpreadElement)[], argumentTarget: ArgumentTarget | undefined): ESExpression[] {\n  const expressionArgs = args.map((arg): ESExpression => {\n    return arg.type === \"SpreadElement\"\n      ? arg.argument\n      : arg;\n  });\n\n  if(typeof argumentTarget !== \"number\"){\n    return getTargetItems(expressionArgs, argumentTarget, \"all\");\n  }\n\n  if(args.length === 0){\n    return [];\n  }\n\n  const index = argumentTarget >= 0\n    ? argumentTarget\n    : args.length + argumentTarget;\n\n  if(index < 0 || index >= args.length){\n    return [];\n  }\n\n  const targetArg = args[index];\n\n  return [targetArg.type === \"SpreadElement\" ? targetArg.argument : targetArg];\n}\n\nfunction getTargetItems<T>(items: T[], target: CallTarget | undefined, defaultTarget: \"all\" | \"first\"): T[] {\n  if(items.length === 0){\n    return [];\n  }\n\n  if(target === \"all\" || target === undefined && defaultTarget === \"all\"){\n    return items;\n  }\n\n  if(target === \"last\"){\n    return [items[items.length - 1]];\n  }\n\n  if(target === undefined || target === \"first\"){\n    return [items[0]];\n  }\n\n  const index = target >= 0\n    ? target\n    : items.length + target;\n\n  if(index < 0 || index >= items.length){\n    return [];\n  }\n\n  return [items[index]];\n}\n\nfunction isTaggedTemplateExpression(node: ESBaseNode): node is ESTaggedTemplateExpression {\n  return node.type === \"TaggedTemplateExpression\";\n}\n\nfunction isTaggedTemplateLiteral(node: ESBaseNode): node is ESTemplateLiteral {\n  return hasESNodeParentExtension(node) && isTaggedTemplateExpression(node.parent);\n}\n\nexport function isESVariableDeclarator(node: ESBaseNode): node is ESVariableDeclarator {\n  return node.type === \"VariableDeclarator\";\n}\n\nfunction isESExportDefaultExpression(node: ESBaseNode): node is ESExpression {\n  if(node.type === \"FunctionDeclaration\"){\n    return false;\n  }\n\n  if(node.type === \"ClassDeclaration\"){\n    return false;\n  }\n\n  return true;\n}\n\nfunction isESVariableSymbol(node: ESBaseNode & Partial<Rule.NodeParentExtension>): node is ESIdentifier {\n  return node.type === \"Identifier\" && !!node.parent && isESVariableDeclarator(node.parent);\n}\n\nexport function hasESNodeParentExtension(node: ESBaseNode): node is Rule.Node & Rule.NodeParentExtension {\n  return \"parent\" in node && !!node.parent;\n}\n\nfunction getBracesByString(ctx: Rule.RuleContext, raw: string): BracesMeta {\n  const closingBraces = raw.trim().startsWith(\"}\") ? \"}\" : undefined;\n  const openingBraces = raw.trim().endsWith(\"${\") ? \"${\" : undefined;\n\n  return {\n    closingBraces,\n    openingBraces\n  };\n}\n\nfunction getIsInterpolated(ctx: Rule.RuleContext, raw: string): boolean {\n  const braces = getBracesByString(ctx, raw);\n  return !!braces.closingBraces || !!braces.openingBraces;\n}\n\nfunction getLeadingComment(ctx: Rule.RuleContext, node: ESNode): string | undefined {\n  try {\n    const token = ctx.sourceCode.getTokenBefore(node, { includeComments: true });\n\n    if(token && isESTokenComment(token)){\n      return token.value.trim();\n    }\n  } catch {}\n}\n\ntype ESTokenWithOptionalComment = {\n  type: string;\n  value?: string;\n};\n\nfunction isESTokenComment(token: ESTokenWithOptionalComment): token is ESTokenWithOptionalComment & { value: string; } {\n  return (token.type === \"Block\" || token.type === \"Line\") && typeof token.value === \"string\";\n}\n\nfunction getStringConcatenationMeta(node: ESNode, isConcatenatedLeft = false, isConcatenatedRight = false): { isConcatenatedLeft: boolean; isConcatenatedRight: boolean; } {\n  if(!hasESNodeParentExtension(node)){\n    return {\n      isConcatenatedLeft,\n      isConcatenatedRight\n    };\n  }\n\n  const parent = node.parent;\n\n  if(parent.type === \"BinaryExpression\" && parent.operator === \"+\"){\n    return getStringConcatenationMeta(\n      parent,\n      isConcatenatedLeft || parent.right === node,\n      isConcatenatedRight || parent.left === node\n    );\n  }\n\n  return getStringConcatenationMeta(parent, isConcatenatedLeft, isConcatenatedRight);\n}\n\n\nexport function getESMatcherFunctions(\n  matchers: SelectorMatcher[],\n  options?: {\n    isStringLikeNode?: (node: ESBaseNode) => boolean;\n  }\n): MatcherFunctions {\n  return matchers.reduce<MatcherFunctions>((matcherFunctions, matcher) => {\n    switch (matcher.type){\n      case MatcherType.AnonymousFunctionReturn: {\n\n        matcherFunctions.push(node => {\n\n          if(isESNode(node) && (\n            isESCallExpression(node) ||\n            isESVariableDeclarator(node)\n          )){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isESNode(node) ||\n            !hasESNodeParentExtension(node) ||\n            !isESAnonymousFunction(node)\n          ){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          // return matchers directly if the arrow function immediately returns\n          if(isESArrowFunctionWithoutBody(node)){\n            return [(node: unknown) => {\n              if(\n                !isESNode(node) ||\n                !hasESNodeParentExtension(node) ||\n                !isESArrowFunctionWithoutBody(node.parent) ||\n                node !== node.parent.body){\n                return MATCHER_RESULT.NO_MATCH;\n              }\n\n              return getESMatcherFunctions(matcher.match, options);\n            }];\n          }\n\n          // create a matcher function that first matches the return statement and then the final matchers\n          return [(node: unknown) => {\n            if(isESNode(node) && (\n              isESCallExpression(node) ||\n              isESArrowFunctionExpression(node) ||\n              isESVariableDeclarator(node) ||\n              isESFunctionExpression(node) ||\n              isESFunctionDeclaration(node)\n            )){\n              return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n            }\n\n            if(\n              !isESNode(node) ||\n              !hasESNodeParentExtension(node) ||\n\n              !isESReturnStatement(node)\n            ){\n              return MATCHER_RESULT.NO_MATCH;\n            }\n\n            return getESMatcherFunctions(matcher.match, options);\n          }];\n\n        });\n        break;\n      }\n      case MatcherType.String: {\n        matcherFunctions.push(node => {\n\n          if(isESNode(node) && (\n            isESCallExpression(node) ||\n            isESArrowFunctionExpression(node) ||\n            isESVariableDeclarator(node) ||\n            isESFunctionExpression(node)\n          )){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isESNode(node) ||\n            !hasESNodeParentExtension(node) ||\n\n            isInsideDisallowedBinaryExpression(node) ||\n            isInsideConditionalExpressionTest(node) ||\n            isInsideLogicalExpressionLeft(node) ||\n            isIndexedAccessLiteral(node) ||\n\n            isESObjectKey(node) ||\n            isInsideObjectValue(node)){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          return isESStringLike(node) || !!options?.isStringLikeNode?.(node);\n        });\n        break;\n      }\n      case MatcherType.ObjectKey: {\n        matcherFunctions.push(node => {\n\n          if(isESNode(node) && (\n            isESCallExpression(node) ||\n            isESArrowFunctionExpression(node) ||\n            isESVariableDeclarator(node) ||\n            isESFunctionExpression(node)\n          )){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isESNode(node) ||\n            !hasESNodeParentExtension(node) ||\n            !isESObjectKey(node) ||\n\n            isInsideDisallowedBinaryExpression(node) ||\n            isInsideConditionalExpressionTest(node) ||\n            isInsideLogicalExpressionLeft(node) ||\n            isInsideMemberExpression(node) ||\n            isIndexedAccessLiteral(node)){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          const path = getESObjectPath(node);\n\n          if(!path || !matcher.path){\n            return MATCHER_RESULT.MATCH;\n          }\n\n          return matchesPathPattern(path, matcher.path);\n        });\n        break;\n      }\n      case MatcherType.ObjectValue: {\n        matcherFunctions.push(node => {\n\n          if(isESNode(node) && (\n            isESCallExpression(node) ||\n            isESArrowFunctionExpression(node) ||\n            isESVariableDeclarator(node) ||\n            isESFunctionExpression(node)\n          )){\n            return MATCHER_RESULT.UNCROSSABLE_BOUNDARY;\n          }\n\n          if(\n            !isESNode(node) ||\n            !hasESNodeParentExtension(node) ||\n            !isInsideObjectValue(node) ||\n\n            isInsideDisallowedBinaryExpression(node) ||\n            isInsideConditionalExpressionTest(node) ||\n            isInsideLogicalExpressionLeft(node) ||\n            isESObjectKey(node) ||\n            isIndexedAccessLiteral(node) ||\n\n            !isESStringLike(node) && !options?.isStringLikeNode?.(node)){\n            return MATCHER_RESULT.NO_MATCH;\n          }\n\n          const path = getESObjectPath(node);\n\n          if(!path || !matcher.path){\n            return MATCHER_RESULT.MATCH;\n          }\n\n          return matchesPathPattern(path, matcher.path);\n        });\n        break;\n      }\n    }\n    return matcherFunctions;\n  }, []);\n}\n"
  },
  {
    "path": "src/parsers/html.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"html\", () => {\n\n  it(\"should match attribute names via regex\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          html: `<img customAttribute=\"b a\" />`,\n          htmlOutput: `<img customAttribute=\"a b\" />`,\n\n          errors: 1,\n          options: [{ attributes: [\".*Attribute\"], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/parsers/html.ts",
    "content": "import {\n  addAttribute,\n  deduplicateLiterals,\n  getContent,\n  getIndentation,\n  matchesName\n} from \"better-tailwindcss:utils/utils.js\";\n\nimport type { AttributeNode, TagNode } from \"es-html-parser\";\nimport type { Rule } from \"eslint\";\n\nimport type { Literal, QuoteMeta } from \"better-tailwindcss:types/ast.js\";\nimport type { AttributeSelector } from \"better-tailwindcss:types/rule.js\";\n\n\nexport function getLiteralsByHTMLAttribute(ctx: Rule.RuleContext, attribute: AttributeNode, selectors: AttributeSelector[]): Literal[] {\n\n  const name = attribute.key.value;\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n    if(!matchesName(selector.name.toLowerCase(), name.toLowerCase())){ return literals; }\n    if(!selector.match){\n      literals.push(...getLiteralsByHTMLAttributeNode(ctx, attribute));\n      return literals;\n    }\n\n    return literals;\n  }, []);\n\n  return literals\n    .filter(deduplicateLiterals)\n    .map(addAttribute(name));\n\n}\n\nexport function getAttributesByHTMLTag(ctx: Rule.RuleContext, node: TagNode): AttributeNode[] {\n  return node.attributes;\n}\n\nexport function getLiteralsByHTMLAttributeNode(ctx: Rule.RuleContext, attribute: AttributeNode): Literal[] {\n\n  const value = attribute.value;\n\n  if(!value){\n    return [];\n  }\n\n  const line = ctx.sourceCode.lines[attribute.loc.start.line - 1];\n  const raw = attribute.startWrapper?.value + value.value + attribute.endWrapper?.value;\n\n  const quotes = getQuotesByHTMLAttribute(ctx, attribute);\n  const indentation = getIndentation(line);\n  const content = getContent(raw, quotes);\n\n  return [{\n    ...quotes,\n    content,\n    indentation,\n    isInterpolated: false,\n    loc: value.loc,\n    range: [value.range[0] - 1, value.range[1] + 1], // include quotes in range\n    raw,\n    supportsMultiline: true,\n    type: \"StringLiteral\"\n  }];\n\n}\n\n\nfunction getQuotesByHTMLAttribute(ctx: Rule.RuleContext, attribute: AttributeNode): QuoteMeta {\n  const openingQuote = attribute.startWrapper?.value;\n  const closingQuote = attribute.endWrapper?.value;\n\n  return {\n    closingQuote: closingQuote === \"'\" || closingQuote === '\"' ? closingQuote : undefined,\n    openingQuote: openingQuote === \"'\" || openingQuote === '\"' ? openingQuote : undefined\n  };\n}\n"
  },
  {
    "path": "src/parsers/jsx.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"jsx\", () => {\n\n  it(\"should match attribute names via regex\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          jsx: `<img customAttribute=\"b a\" />`,\n          jsxOutput: `<img customAttribute=\"a b\" />`,\n\n          errors: 1,\n          options: [{ attributes: [\".*Attribute\"], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  // #119\n  it(\"should not report inside member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: `<img className={classes[\" ignored \"]} />`\n        }\n      ]\n    });\n  });\n\n  // #211\n  it(\"should still handle object values even when they are immediately index accessed\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"<img class={{ key: '  a b c  '}['key']} />\",\n          jsxOutput: \"<img class={{ key: 'a b c'}['key']} />\",\n\n          errors: 2,\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectValue }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #226\n  it(\"should not match index accessed object keys\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          jsx: \"<img class={{ '  a b c  ': '  d e f '}['  a b c  ']} />\",\n\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectKey }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #286\n  it(\"should not match index access string literals\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"<img class={{ ' a b c ': ' d e f '}[' a b c ']} />\",\n          jsxOutput: \"<img class={{ ' a b c ': 'd e f'}[' a b c ']} />\",\n\n          errors: 2,\n\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectValue }]]]\n          }]\n        },\n        {\n          jsx: \"<img class={cx(' a b c ')[' d e f ']} />\",\n          jsxOutput: \"<img class={cx('a b c')[' d e f ']} />\",\n\n          errors: 2,\n\n          options: [{\n            callees: [[\"cx\", [{ match: MatcherType.String }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match default export via variable selector\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `export default \" lint \";`,\n          jsxOutput: `export default \"lint\";`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Variable,\n                name: \"^default$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n});\n\ndescribe(\"astro (jsx)\", () => {\n  it(\"should match astro syntactic sugar\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          astro: `<img class:list=\"b a\" />`,\n          astroOutput: `<img class:list=\"a b\" />`,\n\n          errors: 1,\n          options: [{ order: \"asc\" }]\n        }\n      ]\n    });\n  });\n});\n\ndescribe(\"solid (jsx)\", () => {\n  it(\"should match solid classList attribute\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          jsx: `<div classList={{ \"b a\": true }} />`,\n          jsxOutput: `<div classList={{ \"a b\": true }} />`,\n\n          errors: 1,\n          options: [{ order: \"asc\" }]\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "src/parsers/jsx.ts",
    "content": "import {\n  ES_CONTAINER_TYPES_TO_INSERT_BRACES,\n  ES_CONTAINER_TYPES_TO_REPLACE_QUOTES,\n  getLiteralsByESMatchers,\n  getLiteralsByESTemplateLiteral,\n  getStringLiteralByESStringLiteral,\n  hasESNodeParentExtension,\n  isESNode,\n  isESSimpleStringLiteral,\n  isESTemplateLiteral\n} from \"better-tailwindcss:parsers/es.js\";\nimport { addAttribute, deduplicateLiterals, matchesName } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Rule } from \"eslint\";\nimport type { BaseNode as ESBaseNode, TemplateLiteral as ESTemplateLiteral } from \"estree\";\nimport type { JSXAttribute, BaseNode as JSXBaseNode, JSXExpressionContainer, JSXOpeningElement } from \"estree-jsx\";\n\nimport type { ESSimpleStringLiteral } from \"better-tailwindcss:parsers/es.js\";\nimport type { Literal, LiteralValueQuotes, MultilineMeta } from \"better-tailwindcss:types/ast.js\";\nimport type { AttributeSelector } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const JSX_CONTAINER_TYPES_TO_REPLACE_QUOTES = [\n  ...ES_CONTAINER_TYPES_TO_REPLACE_QUOTES,\n  \"JSXExpressionContainer\"\n];\n\nexport const JSX_CONTAINER_TYPES_TO_INSERT_BRACES = [\n  ...ES_CONTAINER_TYPES_TO_INSERT_BRACES\n];\n\n\nexport function getLiteralsByJSXAttribute(ctx: Rule.RuleContext, attribute: JSXAttribute, selectors: AttributeSelector[]): Literal[] {\n\n  const name = getAttributeName(attribute);\n  const value = attribute.value;\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n    if(!value){ return literals; }\n\n    if(typeof name !== \"string\"){\n      return literals;\n    }\n\n    if(!matchesName(selector.name.toLowerCase(), name.toLowerCase())){ return literals; }\n\n    if(!selector.match){\n      literals.push(...getLiteralsByJSXAttributeValue(ctx, value));\n      return literals;\n    }\n\n    literals.push(...getLiteralsByESMatchers(ctx, value, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals\n    .filter(deduplicateLiterals)\n    .map(addAttribute(name));\n}\n\nexport function getAttributesByJSXElement(ctx: Rule.RuleContext, node: JSXOpeningElement): JSXAttribute[] {\n  return node.attributes.reduce<JSXAttribute[]>((acc, attribute) => {\n    if(isJSXAttribute(attribute)){\n      acc.push(attribute);\n    }\n    return acc;\n  }, []);\n}\n\nfunction getAttributeName(attribute: JSXAttribute): string | undefined {\n  if(attribute.name.type === \"JSXIdentifier\"){\n    return attribute.name.name;\n  }\n  if(attribute.name.type === \"JSXNamespacedName\"){\n    return `${attribute.name.namespace.name}:${attribute.name.name.name}`;\n  }\n}\n\nfunction getLiteralsByJSXAttributeValue(ctx: Rule.RuleContext, value: JSXAttribute[\"value\"]): Literal[] {\n\n  if(!value){ return []; }\n\n  if(isESSimpleStringLiteral(value)){\n    const stringLiteral = getStringLiteralByJSXStringLiteral(ctx, value);\n\n    if(stringLiteral){\n      return [stringLiteral];\n    }\n  }\n\n  if(isJSXExpressionContainerWithESSimpleStringLiteral(value)){\n    const stringLiteral = getStringLiteralByJSXStringLiteral(ctx, value.expression);\n\n    if(stringLiteral){\n      return [stringLiteral];\n    }\n  }\n\n  if(isJSXExpressionContainerWithESTemplateLiteral(value)){\n    return getLiteralsByJSXTemplateLiteral(ctx, value.expression);\n  }\n\n  return [];\n\n}\n\nfunction getStringLiteralByJSXStringLiteral(ctx: Rule.RuleContext, node: ESSimpleStringLiteral): Literal | undefined {\n  const literal = getStringLiteralByESStringLiteral(ctx, node);\n  const multilineQuotes = getMultilineQuotes(node);\n\n  if(!literal){\n    return;\n  }\n\n  return {\n    ...literal,\n    ...multilineQuotes\n  };\n}\n\nfunction getLiteralsByJSXTemplateLiteral(ctx: Rule.RuleContext, node: ESTemplateLiteral): Literal[] {\n  const literals = getLiteralsByESTemplateLiteral(ctx, node);\n\n  return literals.map(literal => {\n    if(!hasESNodeParentExtension(node)){\n      return literal;\n    }\n\n    const multilineQuotes = getMultilineQuotes(node);\n\n    return {\n      ...literal,\n      ...multilineQuotes\n    };\n  });\n}\n\nfunction getMultilineQuotes(node: ESBaseNode & Rule.NodeParentExtension): MultilineMeta {\n  const surroundingBraces = JSX_CONTAINER_TYPES_TO_INSERT_BRACES.includes(node.parent.type);\n  const multilineQuotes: LiteralValueQuotes[] = JSX_CONTAINER_TYPES_TO_REPLACE_QUOTES.includes(node.parent.type)\n    ? [\"`\"]\n    : [];\n\n  return {\n    multilineQuotes,\n    surroundingBraces\n  };\n}\n\nfunction isJSXExpressionContainerWithESSimpleStringLiteral(node: JSXBaseNode): node is JSXExpressionContainer & { expression: ESSimpleStringLiteral; } {\n  return node.type === \"JSXExpressionContainer\" && \"expression\" in node &&\n    isESNode(node.expression) &&\n    isESSimpleStringLiteral(node.expression);\n}\n\nfunction isJSXExpressionContainerWithESTemplateLiteral(node: JSXBaseNode): node is JSXExpressionContainer & { expression: ESTemplateLiteral; } {\n  return node.type === \"JSXExpressionContainer\" && \"expression\" in node &&\n    isESNode(node.expression) &&\n    isESTemplateLiteral(node.expression);\n}\n\nfunction isJSXAttribute(node: JSXBaseNode): node is JSXAttribute {\n  return node.type === \"JSXAttribute\";\n}\n"
  },
  {
    "path": "src/parsers/svelte.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { enforceConsistentLineWrapping } from \"better-tailwindcss:rules/enforce-consistent-line-wrapping.js\";\nimport { noRestrictedClasses } from \"better-tailwindcss:rules/no-restricted-classes.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"svelte\", () => {\n\n  it(\"should match attribute names via regex\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          svelte: `<img customAttribute=\"b a\" />`,\n          svelteOutput: `<img customAttribute=\"a b\" />`,\n\n          errors: 1,\n          options: [{ attributes: [\".*Attribute\"], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  // #42\n  it(\"should work with shorthand attributes\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          svelte: `<script>let disabled = true;</script><img class=\"c b a\" {disabled} />`,\n          svelteOutput: `<script>let disabled = true;</script><img class=\"a b c\" {disabled} />`,\n\n          errors: 1,\n          options: [{ order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should change the quotes in expressions to backticks\", () => {\n    const singleLine = \"a b c d e f\";\n    const multiLine = dedent`\n      a b c\n      d e f\n    `;\n\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          svelte: `<img class={true ? '${singleLine}' : '${singleLine}'} />`,\n          svelteOutput: `<img class={true ? \\`${multiLine}\\` : \\`${multiLine}\\`} />`,\n\n          errors: 2,\n          options: [{ classesPerLine: 3 }]\n        }\n      ]\n    });\n  });\n\n  // #119\n  it(\"should not report inside member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          svelte: `<img class={classes[\" ignored \"]} />`\n        }\n      ]\n    });\n  });\n\n  // #211\n  it(\"should still handle object values even when they are immediately index accessed\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          svelte: \"<img class={{ key: '  a b c  '}['key']} />\",\n          svelteOutput: \"<img class={{ key: 'a b c'}['key']} />\",\n\n          errors: 2,\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectValue }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #226\n  it(\"should not match index accessed object keys\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          svelte: \"<img class={{ '  a b c  ': '  d e f '}['  a b c  ']} />\",\n\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectKey }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #237\n  it(\"should keep interpolations in normal string literals\", () => {\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          svelte: `<img class=\"a b {someVar ? 'c' : 'd'} md:e\" />`,\n          svelteOutput: `<img class=\"\\n  a b\\n  {someVar ? 'c' : 'd'}\\n  md:e\\n\" />`,\n\n          errors: 2\n        }\n      ],\n      valid: [\n        {\n          svelte: `<img class=\"\\n  flex\\n  {disabled ? 'cursor-default' : 'cursor-pointer'}\" />`\n        }\n      ]\n    });\n  });\n\n  // https://svelte.dev/docs/svelte/class#The-class:-directive\n  it(\"should class name directive\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          svelte: `<img class:restricted={true} />`,\n          svelteOutput: `<img class:allowed={true} />`,\n\n          errors: 1,\n          options: [{\n            restrict: [\n              { fix: \"allowed\", pattern: \"restricted\" }\n            ]\n          }]\n        },\n        {\n          svelte: `<img class:restricted />`,\n          svelteOutput: `<img class:allowed />`,\n\n          errors: 1,\n          options: [{\n            restrict: [\n              { fix: \"allowed\", pattern: \"restricted\" }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match default export via variable selector\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          svelte: `<script>export default \" lint \";</script>`,\n          svelteOutput: `<script>export default \"lint\";</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Variable,\n                name: \"^default$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "src/parsers/svelte.ts",
    "content": "import {\n  ES_CONTAINER_TYPES_TO_REPLACE_QUOTES,\n  getESMatcherFunctions,\n  getLiteralsByESLiteralNode,\n  hasESNodeParentExtension,\n  isESStringLike\n} from \"better-tailwindcss:parsers/es.js\";\nimport { getLiteralNodesByMatchers } from \"better-tailwindcss:utils/matchers.js\";\nimport {\n  addAttribute,\n  deduplicateLiterals,\n  getContent,\n  getIndentation,\n  getQuotes,\n  getWhitespace,\n  matchesName\n} from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Rule } from \"eslint\";\nimport type { BaseNode as ESBaseNode, Node as ESNode } from \"estree\";\nimport type {\n  SvelteAttachTag,\n  SvelteAttribute,\n  SvelteDirective,\n  SvelteGenericsDirective,\n  SvelteLiteral,\n  SvelteMustacheTagText,\n  SvelteName,\n  SvelteShorthandAttribute,\n  SvelteSpecialDirective,\n  SvelteSpreadAttribute,\n  SvelteStartTag,\n  SvelteStyleDirective\n} from \"svelte-eslint-parser/lib/ast/index.js\";\n\nimport type {\n  BracesMeta,\n  Literal,\n  LiteralValueQuotes,\n  MultilineMeta,\n  StringLiteral\n} from \"better-tailwindcss:types/ast.js\";\nimport type { AttributeSelector, MatcherFunctions, SelectorMatcher } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const SVELTE_CONTAINER_TYPES_TO_REPLACE_QUOTES = [\n  ...ES_CONTAINER_TYPES_TO_REPLACE_QUOTES,\n  \"SvelteMustacheTag\"\n];\n\nexport const SVELTE_CONTAINER_TYPES_TO_INSERT_BRACES: string[] = [\n];\n\n\nexport function getAttributesBySvelteTag(ctx: Rule.RuleContext, node: SvelteStartTag): SvelteAttribute[] {\n  return node.attributes.reduce<SvelteAttribute[]>((acc, attribute) => {\n    if(isSvelteAttribute(attribute)){\n      acc.push(attribute);\n    }\n    return acc;\n  }, []);\n}\n\nexport function getDirectivesBySvelteTag(ctx: Rule.RuleContext, node: SvelteStartTag): SvelteDirective[] {\n  return node.attributes.reduce<SvelteDirective[]>((acc, attribute) => {\n    if(isSvelteDirective(attribute)){\n      acc.push(attribute);\n    }\n    return acc;\n  }, []);\n}\n\nexport function getLiteralsBySvelteAttribute(ctx: Rule.RuleContext, attribute: SvelteAttribute, selectors: AttributeSelector[]): Literal[] {\n\n  // skip shorthand attributes #42\n  if(!Array.isArray(attribute.value)){\n    return [];\n  }\n\n  const name = attribute.key.name;\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n\n    for(const value of attribute.value){\n      if(!matchesName(selector.name.toLowerCase(), name.toLowerCase())){ continue; }\n\n      if(!selector.match){\n        literals.push(...getLiteralsBySvelteLiteralNode(ctx, value));\n        continue;\n      }\n\n      literals.push(...getLiteralsBySvelteMatchers(ctx, value, selector.match));\n    }\n\n    return literals;\n  }, []);\n\n  return literals\n    .filter(deduplicateLiterals)\n    .map(addAttribute(name));\n\n}\n\nexport function getLiteralsBySvelteDirective(ctx: Rule.RuleContext, directive: SvelteDirective, selectors: AttributeSelector[]): Literal[] {\n\n  if(directive.kind !== \"Class\"){\n    return [];\n  }\n\n  const name = `class:${directive.key.name.name}`;\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n\n    if(!matchesName(selector.name.toLowerCase(), name.toLowerCase())){ return literals; }\n\n    if(!selector.match){\n      return literals;\n    }\n\n    literals.push(...getLiteralsBySvelteMatchers(ctx, directive.key.name, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals\n    .filter(deduplicateLiterals)\n    .map(addAttribute(name));\n\n}\n\nfunction getLiteralsBySvelteMatchers(ctx: Rule.RuleContext, node: ESBaseNode, matchers: SelectorMatcher[]): Literal[] {\n  const matcherFunctions = getSvelteMatcherFunctions(matchers);\n\n  const literalNodes = getLiteralNodesByMatchers<ESBaseNode>(ctx, node, matcherFunctions);\n  const literals = literalNodes.flatMap(literalNode => getLiteralsBySvelteLiteralNode(ctx, literalNode));\n\n  return literals.filter(deduplicateLiterals);\n}\n\nfunction getLiteralsBySvelteLiteralNode(ctx: Rule.RuleContext, node: ESBaseNode): Literal[] {\n\n  if(isSvelteStringLiteral(node)){\n    const stringLiteral = getStringLiteralBySvelteStringLiteral(ctx, node);\n\n    if(stringLiteral){\n      return [stringLiteral];\n    }\n  }\n\n  if(isSvelteName(node)){\n    const stringLiteral = getStringLiteralBySvelteName(ctx, node);\n\n    if(stringLiteral){\n      return [stringLiteral];\n    }\n  }\n\n  if(isSvelteMustacheTag(node)){\n    return getLiteralsBySvelteLiteralNode(ctx, node.expression);\n  }\n\n  if(isESStringLike(node)){\n    return getLiteralsBySvelteESLiteralNode(ctx, node);\n  }\n\n  return [];\n\n}\n\nfunction getLiteralsBySvelteESLiteralNode(ctx: Rule.RuleContext, node: ESBaseNode): Literal[] {\n  const literals = getLiteralsByESLiteralNode(ctx, node);\n\n  return literals.map(literal => {\n    if(!hasESNodeParentExtension(node)){ return literal; }\n\n    const multilineQuotes = getMultilineQuotes(node);\n\n    return {\n      ...literal,\n      ...multilineQuotes\n    };\n  });\n}\n\nfunction getStringLiteralBySvelteName(ctx: Rule.RuleContext, node: SvelteName): StringLiteral {\n\n  const raw = node.name;\n\n  const braces = getBracesByString(ctx, raw);\n  const isInterpolated = getIsInterpolated(ctx, raw);\n  const quotes = getQuotes(raw);\n  const content = getContent(raw, quotes, braces);\n  const whitespaces = getWhitespace(content);\n  const line = ctx.sourceCode.lines[isInterpolated ? node.parent.loc.start.line - 1 : node.loc.start.line - 1];\n  const indentation = getIndentation(line);\n  const multilineQuotes = getMultilineQuotes(node);\n\n  return {\n    ...whitespaces,\n    ...quotes,\n    ...braces,\n    ...multilineQuotes,\n    content,\n    indentation,\n    isInterpolated,\n    loc: node.loc,\n    range: node.range, // include quotes in range\n    raw,\n    supportsMultiline: false,\n    type: \"StringLiteral\"\n  };\n\n}\n\nfunction getStringLiteralBySvelteStringLiteral(ctx: Rule.RuleContext, node: SvelteLiteral): StringLiteral | undefined {\n\n  const raw = ctx.sourceCode.getText(node as unknown as ESNode, 1, 1);\n\n  const braces = getBracesByString(ctx, raw);\n  const isInterpolated = getIsInterpolated(ctx, raw);\n  const quotes = getQuotes(raw);\n  const content = getContent(raw, quotes, braces);\n  const whitespaces = getWhitespace(content);\n  const line = ctx.sourceCode.lines[isInterpolated ? node.parent.loc.start.line - 1 : node.loc.start.line - 1];\n  const indentation = getIndentation(line);\n  const multilineQuotes = getMultilineQuotes(node);\n\n  return {\n    ...whitespaces,\n    ...quotes,\n    ...braces,\n    ...multilineQuotes,\n    content,\n    indentation,\n    isInterpolated,\n    loc: node.loc,\n    range: [node.range[0] - 1, node.range[1] + 1], // include quotes in range\n    raw,\n    supportsMultiline: true,\n    type: \"StringLiteral\"\n  };\n\n}\n\nfunction getBracesByString(ctx: Rule.RuleContext, raw: string): BracesMeta {\n  const closingBraces = raw.trim().startsWith(\"}\") ? \"}\" : undefined;\n  const openingTemplateBraces = raw.trim().endsWith(\"${\") ? \"${\" : undefined;\n  const openingBrace = raw.trim().endsWith(\"{\") ? \"{\" : undefined;\n  const openingBraces = openingTemplateBraces ?? openingBrace;\n\n  return {\n    closingBraces,\n    openingBraces\n  };\n}\n\nfunction getIsInterpolated(ctx: Rule.RuleContext, raw: string): boolean {\n  const braces = getBracesByString(ctx, raw);\n  return !!braces.closingBraces || !!braces.openingBraces;\n}\n\nfunction getMultilineQuotes(node: ESBaseNode & Rule.NodeParentExtension | SvelteLiteral | SvelteName): MultilineMeta {\n  const surroundingBraces = SVELTE_CONTAINER_TYPES_TO_INSERT_BRACES.includes(node.parent.type);\n  const multilineQuotes: LiteralValueQuotes[] = SVELTE_CONTAINER_TYPES_TO_REPLACE_QUOTES.includes(node.parent.type)\n    ? [\"'\", \"\\\"\", \"`\"]\n    : [];\n\n  return {\n    multilineQuotes,\n    surroundingBraces\n  };\n}\n\nfunction isSvelteAttribute(node:\n  | SvelteAttachTag\n  | SvelteAttribute\n  | SvelteDirective\n  | SvelteGenericsDirective\n  | SvelteShorthandAttribute\n  | SvelteSpecialDirective\n  | SvelteSpreadAttribute\n  | SvelteStyleDirective): node is SvelteAttribute {\n  return node.type === \"SvelteAttribute\";\n}\n\nfunction isSvelteDirective(node: ESBaseNode): node is SvelteDirective {\n  return node.type === \"SvelteDirective\";\n}\n\nfunction isSvelteStringLiteral(node: ESBaseNode): node is SvelteLiteral {\n  return node.type === \"SvelteLiteral\";\n}\n\nfunction isSvelteName(node: ESBaseNode): node is SvelteName {\n  return node.type === \"SvelteName\";\n}\n\nfunction isSvelteMustacheTag(node: ESBaseNode): node is SvelteMustacheTagText {\n  return node.type === \"SvelteMustacheTag\" &&\n    \"kind\" in node && node.kind === \"text\";\n}\n\nfunction getSvelteMatcherFunctions(matchers: SelectorMatcher[]): MatcherFunctions {\n  return getESMatcherFunctions(matchers, {\n    isStringLikeNode(node) {\n      return isSvelteName(node) || isSvelteStringLiteral(node);\n    }\n  });\n}\n"
  },
  {
    "path": "src/parsers/vue.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { enforceConsistentLineWrapping } from \"better-tailwindcss:rules/enforce-consistent-line-wrapping.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\nimport { MatcherType, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\ndescribe(\"vue\", () => {\n\n  it(\"should match attribute names via regex\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          vue: `<template><img customAttribute=\"b a\" /></template>`,\n          vueOutput: `<template><img customAttribute=\"a b\" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [\".*Attribute\"], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should work in objects in bound classes\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          vue: `<template><img v-bind:class=\"{ 'c b a': condition === 'c b a' }\" /></template>`,\n          vueOutput: `<template><img v-bind:class=\"{ 'a b c': condition === 'c b a' }\" /></template>`,\n\n          errors: 1,\n          options: [{ order: \"asc\" }]\n        },\n        {\n          vue: `<template><img :class=\"{ 'c b a': condition === 'c b a' }\" /></template>`,\n          vueOutput: `<template><img :class=\"{ 'a b c': condition === 'c b a' }\" /></template>`,\n\n          errors: 1,\n          options: [{ order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should work in arrays in bound classes\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          vue: `<template><img v-bind:class=\"[condition === 'c b a' ? 'c b a' : 'f e d']\" /></template>`,\n          vueOutput: `<template><img v-bind:class=\"[condition === 'c b a' ? 'a b c' : 'd e f']\" /></template>`,\n\n          errors: 2,\n          options: [{ order: \"asc\" }]\n        },\n        {\n          vue: `<template><img :class=\"[condition === 'c b a' ? 'c b a' : 'f e d']\" /></template>`,\n          vueOutput: `<template><img :class=\"[condition === 'c b a' ? 'a b c' : 'd e f']\" /></template>`,\n\n          errors: 2,\n          options: [{ order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should evaluate bound classes\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          vue: `<template><img v-bind:class=\"defined('c b a')\" /></template>`,\n          vueOutput: `<template><img v-bind:class=\"defined('a b c')\" /></template>`,\n\n          errors: 1,\n          options: [{ callees: [\"defined\"], order: \"asc\" }]\n        },\n        {\n          vue: `<template><img :class=\"defined('c b a')\" /></template>`,\n          vueOutput: `<template><img :class=\"defined('a b c')\" /></template>`,\n\n          errors: 1,\n          options: [{ callees: [\"defined\"], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should automatically prefix bound classes\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          vue: `<template><img v-bind:custom-class=\"['c b a']\" /></template>`,\n          vueOutput: `<template><img v-bind:custom-class=\"['a b c']\" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [[\":custom-class\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n        },\n        {\n          vue: `<template><img :custom-class=\"['c b a']\" /></template>`,\n          vueOutput: `<template><img :custom-class=\"['a b c']\" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [[\"v-bind:custom-class\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match bound classes via regex\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          vue: `<template><img v-bind:testStyles=\"['c b a']\" /></template>`,\n          vueOutput: `<template><img v-bind:testStyles=\"['a b c']\" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [[\":.*Styles$\", [{ match: MatcherType.String }]]], order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  // #95\n  it(\"should change the quotes in expressions to backticks\", () => {\n    const singleLine = \"a b c d e f\";\n    const multiLine = dedent`\n      a b c\n      d e f\n    `;\n\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          vue: `<template><img :class=\"[true ? '${singleLine}' : '${singleLine}']\" /></template>`,\n          vueOutput: `<template><img :class=\"[true ? \\`${multiLine}\\` : \\`${multiLine}\\`]\" /></template>`,\n\n          errors: 2,\n          options: [{ classesPerLine: 3 }]\n        }\n      ]\n    });\n  });\n\n  // #119\n  it(\"should not report inside member expressions\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          vue: `<template><img :class=\"[ui[' ignored ']]\" /></template>`\n        }\n      ]\n    });\n  });\n\n  // #211\n  it(\"should still handle object values even when they are immediately index accessed\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          vue: `<template><img :class=\"{ key: '  a b c  ' }['key']\" /></template>`,\n          vueOutput: `<template><img :class=\"{ key: 'a b c' }['key']\" /></template>`,\n\n          errors: 2,\n          options: [{\n            attributes: [[\".*\", [{ match: MatcherType.ObjectValue }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #226\n  it(\"should not match index accessed object keys\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          vue: \"<template><img class={{ '  a b c  ': '  d e f '}['  a b c  ']} /></template>\",\n\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectKey }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match default export via variable selector\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          vue: `<script>export default \" lint \";</script>`,\n          vueOutput: `<script>export default \"lint\";</script>`,\n\n          errors: 2,\n          options: [{\n            selectors: [\n              {\n                kind: SelectorKind.Variable,\n                name: \"^default$\"\n              }\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/parsers/vue.ts",
    "content": "import {\n  ES_CONTAINER_TYPES_TO_INSERT_BRACES,\n  ES_CONTAINER_TYPES_TO_REPLACE_QUOTES,\n  getESMatcherFunctions,\n  getLiteralsByESLiteralNode,\n  hasESNodeParentExtension,\n  isESStringLike\n} from \"better-tailwindcss:parsers/es.js\";\nimport { getLiteralNodesByMatchers } from \"better-tailwindcss:utils/matchers.js\";\nimport {\n  addAttribute,\n  deduplicateLiterals,\n  getContent,\n  getIndentation,\n  getQuotes,\n  getWhitespace,\n  matchesName\n} from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Rule } from \"eslint\";\nimport type { BaseNode as ESBaseNode, Node as ESNode } from \"estree\";\nimport type { AST } from \"vue-eslint-parser\";\n\nimport type { Literal, LiteralValueQuotes, MultilineMeta, StringLiteral } from \"better-tailwindcss:types/ast.js\";\nimport type { AttributeSelector, MatcherFunctions, SelectorMatcher } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const VUE_CONTAINER_TYPES_TO_REPLACE_QUOTES = [\n  ...ES_CONTAINER_TYPES_TO_REPLACE_QUOTES\n];\n\nexport const VUE_CONTAINER_TYPES_TO_INSERT_BRACES = [\n  ...ES_CONTAINER_TYPES_TO_INSERT_BRACES\n];\n\n\nexport function getAttributesByVueStartTag(ctx: Rule.RuleContext, node: AST.VStartTag): (AST.VAttribute | AST.VDirective)[] {\n  return node.attributes;\n}\n\nexport function getLiteralsByVueAttribute(ctx: Rule.RuleContext, attribute: AST.VAttribute | AST.VDirective, selectors: AttributeSelector[]): Literal[] {\n\n  if(attribute.value === null){\n    return [];\n  }\n\n  const name = getVueAttributeName(attribute);\n  const value = attribute.value;\n\n  const literals = selectors.reduce<Literal[]>((literals, selector) => {\n    if(!matchesName(getVueBoundName(selector.name).toLowerCase(), name?.toLowerCase())){ return literals; }\n\n    if(!selector.match){\n      literals.push(...getLiteralsByVueLiteralNode(ctx, value));\n      return literals;\n    }\n\n    literals.push(...getLiteralsByVueMatchers(ctx, value, selector.match));\n\n    return literals;\n  }, []);\n\n  return literals\n    .filter(deduplicateLiterals)\n    .map(addAttribute(name));\n\n}\n\nfunction getLiteralsByVueLiteralNode(ctx: Rule.RuleContext, node: ESBaseNode): Literal[] {\n\n  if(!hasESNodeParentExtension(node)){ return []; }\n\n  if(isVueLiteralNode(node)){\n    const literal = getStringLiteralByVueStringLiteral(ctx, node);\n    return [literal];\n  }\n\n  if(isESStringLike(node)){\n    return getLiteralsByVueESLiteralNode(ctx, node);\n  }\n\n  return [];\n}\n\n\nfunction getLiteralsByVueMatchers(ctx: Rule.RuleContext, node: ESBaseNode, matchers: SelectorMatcher[]): Literal[] {\n  const matcherFunctions = getVueMatcherFunctions(matchers);\n\n  const literalNodes = getLiteralNodesByMatchers<ESBaseNode>(ctx, node, matcherFunctions);\n  const literals = literalNodes.flatMap(literalNode => getLiteralsByVueLiteralNode(ctx, literalNode));\n\n  return literals.filter(deduplicateLiterals);\n}\n\nfunction getLiteralsByVueESLiteralNode(ctx: Rule.RuleContext, node: ESBaseNode & Rule.NodeParentExtension): Literal[] {\n  const literals = getLiteralsByESLiteralNode(ctx, node);\n\n  return literals.map(literal => {\n    const multilineQuotes = getMultilineQuotes(node);\n\n    return {\n      ...literal,\n      ...multilineQuotes\n    };\n  });\n}\n\nfunction getStringLiteralByVueStringLiteral(ctx: Rule.RuleContext, node: AST.VLiteral): StringLiteral {\n\n  const raw = ctx.sourceCode.getText(node as unknown as ESNode);\n  const line = ctx.sourceCode.lines[node.loc.start.line - 1];\n\n  const quotes = getQuotes(raw);\n  const content = getContent(raw, quotes);\n  const whitespaces = getWhitespace(content);\n  const indentation = getIndentation(line);\n  const multilineQuotes = getMultilineQuotes(node);\n\n  return {\n    ...whitespaces,\n    ...quotes,\n    ...multilineQuotes,\n    content,\n    indentation,\n    loc: node.loc,\n    priorLiterals: [],\n    range: [node.range[0], node.range[1]],\n    raw,\n    supportsMultiline: true,\n    type: \"StringLiteral\"\n  };\n\n}\n\nfunction getMultilineQuotes(node: ESBaseNode & Rule.NodeParentExtension | AST.VLiteral): MultilineMeta {\n  const surroundingBraces = VUE_CONTAINER_TYPES_TO_INSERT_BRACES.includes(node.parent.type);\n  const multilineQuotes: LiteralValueQuotes[] = VUE_CONTAINER_TYPES_TO_REPLACE_QUOTES.includes(node.parent.type)\n    ? [\"`\"]\n    : [];\n\n  return {\n    multilineQuotes,\n    surroundingBraces\n  };\n}\n\nfunction getVueBoundName(name: string): string {\n  return name.startsWith(\":\") ? `v-bind:${name.slice(1)}` : name;\n}\n\nfunction getVueAttributeName(attribute: AST.VAttribute | AST.VDirective): string | undefined {\n  if(isVueAttribute(attribute)){\n    return attribute.key.name;\n  }\n\n  if(isVueDirective(attribute)){\n    if(attribute.key.argument?.type === \"VIdentifier\"){\n      return `v-${attribute.key.name.name}:${attribute.key.argument.name}`;\n    }\n  }\n}\n\nfunction isVueAttribute(attribute: AST.VAttribute | AST.VDirective): attribute is AST.VAttribute {\n  return attribute.key.type === \"VIdentifier\";\n}\n\nfunction isVueDirective(attribute: AST.VAttribute | AST.VDirective): attribute is AST.VDirective {\n  return attribute.key.type === \"VDirectiveKey\";\n}\n\nfunction isVueLiteralNode(node: ESBaseNode): node is AST.VLiteral {\n  return node.type === \"VLiteral\";\n}\n\nfunction getVueMatcherFunctions(matchers: SelectorMatcher[]): MatcherFunctions {\n  return getESMatcherFunctions(matchers, {\n    isStringLikeNode: isVueLiteralNode\n  });\n}\n"
  },
  {
    "path": "src/rules/enforce-canonical-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceCanonicalClasses } from \"better-tailwindcss:rules/enforce-canonical-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css } from \"better-tailwindcss:tests/utils/template.js\";\nimport { values } from \"better-tailwindcss:tests/utils/values.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version.js\";\n\n\ndescribe.runIf(getTailwindCSSVersion().major >= 4)(enforceCanonicalClasses.name, () => {\n\n  it(\"should convert unnecessary arbitrary value to canonical class\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"[color:red]/100\" />`,\n          angularOutput: `<img class=\"text-[red]\" />`,\n          html: `<img class=\"[color:red]/100\" />`,\n          htmlOutput: `<img class=\"text-[red]\" />`,\n          jsx: `() => <img class=\"[color:red]/100\" />`,\n          jsxOutput: `() => <img class=\"text-[red]\" />`,\n          svelte: `<img class=\"[color:red]/100\" />`,\n          svelteOutput: `<img class=\"text-[red]\" />`,\n          vue: `<template><img class=\"[color:red]/100\" /></template>`,\n          vueOutput: `<template><img class=\"text-[red]\" /></template>`,\n\n          errors: [{\n            message: values(enforceCanonicalClasses.messages!.single, {\n              canonicalClass: \"text-[red]\",\n              className: \"[color:red]/100\"\n            })\n          }],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            entryPoint: \"styles.css\"\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should convert multiple unnecessary arbitrary values to canonical classes\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"[@media_print]:flex [color:red]/50\" />`,\n          angularOutput: `<img class=\"print:flex text-[red]/50\" />`,\n          html: `<img class=\"[@media_print]:flex [color:red]/50\" />`,\n          htmlOutput: `<img class=\"print:flex text-[red]/50\" />`,\n          jsx: `() => <img class=\"[@media_print]:flex [color:red]/50\" />`,\n          jsxOutput: `() => <img class=\"print:flex text-[red]/50\" />`,\n          svelte: `<img class=\"[@media_print]:flex [color:red]/50\" />`,\n          svelteOutput: `<img class=\"print:flex text-[red]/50\" />`,\n          vue: `<template><img class=\"[@media_print]:flex [color:red]/50\" /></template>`,\n          vueOutput: `<template><img class=\"print:flex text-[red]/50\" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.single, {\n                canonicalClass: \"print:flex\",\n                className: \"[@media_print]:flex\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.single, {\n                canonicalClass: \"text-[red]/50\",\n                className: \"[color:red]/50\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            entryPoint: \"styles.css\"\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should collapse multiple utilities into a single utility\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"w-10 flex h-10 top-0 underline right-0 bottom-0 left-0\" />`,\n          angularOutput: `<img class=\"size-10 flex  inset-0 underline   \" />`,\n          html: `<img class=\"w-10 flex h-10 top-0 underline right-0 bottom-0 left-0\" />`,\n          htmlOutput: `<img class=\"size-10 flex  inset-0 underline   \" />`,\n          jsx: `() => <img class=\"w-10 flex h-10 top-0 underline right-0 bottom-0 left-0\" />`,\n          jsxOutput: `() => <img class=\"size-10 flex  inset-0 underline   \" />`,\n          svelte: `<img class=\"w-10 flex h-10 top-0 underline right-0 bottom-0 left-0\" />`,\n          svelteOutput: `<img class=\"size-10 flex  inset-0 underline   \" />`,\n          vue: `<template><img class=\"w-10 flex h-10 top-0 underline right-0 bottom-0 left-0\" /></template>`,\n          vueOutput: `<template><img class=\"size-10 flex  inset-0 underline   \" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"inset-0\",\n                classNames: \"top-0, right-0, bottom-0, left-0\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"inset-0\",\n                classNames: \"top-0, right-0, bottom-0, left-0\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"inset-0\",\n                classNames: \"top-0, right-0, bottom-0, left-0\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"inset-0\",\n                classNames: \"top-0, right-0, bottom-0, left-0\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            collapse: true,\n            entryPoint: \"styles.css\"\n          }]\n        },\n        {\n          angular: `<img class=\"w-10 h-10 flex\" />`,\n          angularOutput: `<img class=\"size-10  flex\" />`,\n          html: `<img class=\"w-10 h-10 flex\" />`,\n          htmlOutput: `<img class=\"size-10  flex\" />`,\n          jsx: `() => <img class=\"w-10 h-10 flex\" />`,\n          jsxOutput: `() => <img class=\"size-10  flex\" />`,\n          svelte: `<img class=\"w-10 h-10 flex\" />`,\n          svelteOutput: `<img class=\"size-10  flex\" />`,\n          vue: `<template><img class=\"w-10 h-10 flex\" /></template>`,\n          vueOutput: `<template><img class=\"size-10  flex\" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            collapse: true,\n            entryPoint: \"styles.css\"\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should convert size correctly\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"w-[20px] h-[20px]\" />`,\n          angularOutput: `<img class=\"size-5 \" />`,\n          html: `<img class=\"w-[20px] h-[20px]\" />`,\n          htmlOutput: `<img class=\"size-5 \" />`,\n          jsx: `() => <img class=\"w-[20px] h-[20px]\" />`,\n          jsxOutput: `() => <img class=\"size-5 \" />`,\n          svelte: `<img class=\"w-[20px] h-[20px]\" />`,\n          svelteOutput: `<img class=\"size-5 \" />`,\n          vue: `<template><img class=\"w-[20px] h-[20px]\" /></template>`,\n          vueOutput: `<template><img class=\"size-5 \" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-5\",\n                classNames: \"w-[20px], h-[20px]\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-5\",\n                classNames: \"w-[20px], h-[20px]\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          settings: {\n            \"better-tailwindcss\": {\n              entryPoint: \"styles.css\",\n              rootFontSize: 16\n            }\n          }\n        }\n      ]\n    });\n  });\n\n  it(\"should convert to logical properties\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"mr-2 ml-2\" />`,\n          angularOutput: `<img class=\"mx-2 \" />`,\n          html: `<img class=\"mr-2 ml-2\" />`,\n          htmlOutput: `<img class=\"mx-2 \" />`,\n          jsx: `() => <img class=\"mr-2 ml-2\" />`,\n          jsxOutput: `() => <img class=\"mx-2 \" />`,\n          svelte: `<img class=\"mr-2 ml-2\" />`,\n          svelteOutput: `<img class=\"mx-2 \" />`,\n          vue: `<template><img class=\"mr-2 ml-2\" /></template>`,\n          vueOutput: `<template><img class=\"mx-2 \" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"mx-2\",\n                classNames: \"mr-2, ml-2\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"mx-2\",\n                classNames: \"mr-2, ml-2\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            entryPoint: \"styles.css\",\n            logical: true,\n            rootFontSize: 16\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should still work when unknown classes are passed\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"w-10 h-10 unknown\" />`,\n          angularOutput: `<img class=\"size-10  unknown\" />`,\n          html: `<img class=\"w-10 h-10 unknown\" />`,\n          htmlOutput: `<img class=\"size-10  unknown\" />`,\n          jsx: `() => <img class=\"w-10 h-10 unknown\" />`,\n          jsxOutput: `() => <img class=\"size-10  unknown\" />`,\n          svelte: `<img class=\"w-10 h-10 unknown\" />`,\n          svelteOutput: `<img class=\"size-10  unknown\" />`,\n          vue: `<template><img class=\"w-10 h-10 unknown\" /></template>`,\n          vueOutput: `<template><img class=\"size-10  unknown\" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            collapse: true,\n            entryPoint: \"styles.css\"\n          }]\n        }\n      ]\n    });\n  });\n\n  // #369\n  it(\"should be possible to ignore classes that match specific patterns\", () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"-mt-[0.04in] [color:red]/100\" />`,\n          angularOutput: `<img class=\"-mt-[0.04in] text-[red]\" />`,\n          html: `<img class=\"-mt-[0.04in] [color:red]/100\" />`,\n          htmlOutput: `<img class=\"-mt-[0.04in] text-[red]\" />`,\n          jsx: `() => <img class=\"-mt-[0.04in] [color:red]/100\" />`,\n          jsxOutput: `() => <img class=\"-mt-[0.04in] text-[red]\" />`,\n          svelte: `<img class=\"-mt-[0.04in] [color:red]/100\" />`,\n          svelteOutput: `<img class=\"-mt-[0.04in] text-[red]\" />`,\n          vue: `<template><img class=\"-mt-[0.04in] [color:red]/100\" /></template>`,\n          vueOutput: `<template><img class=\"-mt-[0.04in] text-[red]\" /></template>`,\n\n          errors: [{\n            message: values(enforceCanonicalClasses.messages!.single, {\n              canonicalClass: \"text-[red]\",\n              className: \"[color:red]/100\"\n            })\n          }],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            entryPoint: \"styles.css\",\n            ignore: [\"^\\\\S*\\\\[.*in\\\\]$\"]\n          }]\n        }\n      ]\n    });\n  });\n\n  // #304\n  it(\"should not remove unrelated classes when simplifying\", { timeout: 30_000 }, () => {\n    lint(enforceCanonicalClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"flex grid aspect-[4/3]\" />`,\n          angularOutput: `<img class=\"flex grid aspect-4/3\" />`,\n          html: \"<img class='flex grid aspect-[4/3]' />\",\n          htmlOutput: \"<img class='flex grid aspect-4/3' />\",\n          jsx: `() => <img className=\"flex grid aspect-[4/3]\" />`,\n          jsxOutput: `() => <img className=\"flex grid aspect-4/3\" />`,\n          svelte: `<img class=\"flex grid aspect-[4/3]\" />`,\n          svelteOutput: `<img class=\"flex grid aspect-4/3\" />`,\n          vue: `<template><img class=\"flex grid aspect-[4/3]\" /></template>`,\n          vueOutput: `<template><img class=\"flex grid aspect-4/3\" /></template>`,\n\n          errors: [{\n            message: values(enforceCanonicalClasses.messages!.single, {\n              canonicalClass: \"aspect-4/3\",\n              className: \"aspect-[4/3]\"\n            })\n          }],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            collapse: true,\n            entryPoint: \"styles.css\"\n          }]\n        },\n        {\n          angular: `<img class=\"flex grid block aspect-[4/3] inline\" />`,\n          angularOutput: `<img class=\"flex grid block aspect-4/3 inline\" />`,\n          html: `<img class=\"flex grid block aspect-[4/3] inline\" />`,\n          htmlOutput: `<img class=\"flex grid block aspect-4/3 inline\" />`,\n          jsx: `() => <img className=\"flex grid block aspect-[4/3] inline\" />`,\n          jsxOutput: `() => <img className=\"flex grid block aspect-4/3 inline\" />`,\n          svelte: `<img class=\"flex grid block aspect-[4/3] inline\" />`,\n          svelteOutput: `<img class=\"flex grid block aspect-4/3 inline\" />`,\n          vue: `<template><img class=\"flex grid block aspect-[4/3] inline\" /></template>`,\n          vueOutput: `<template><img class=\"flex grid block aspect-4/3 inline\" /></template>`,\n\n          errors: [{\n            message: values(enforceCanonicalClasses.messages!.single, {\n              canonicalClass: \"aspect-4/3\",\n              className: \"aspect-[4/3]\"\n            })\n          }],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            collapse: true,\n            entryPoint: \"styles.css\"\n          }]\n        },\n        {\n          angular: `<img class=\"w-10 h-10 flex grid aspect-[4/3] inline\" />`,\n          angularOutput: `<img class=\"size-10  flex grid aspect-4/3 inline\" />`,\n          html: `<img class=\"w-10 h-10 flex grid aspect-[4/3] inline\" />`,\n          htmlOutput: `<img class=\"size-10  flex grid aspect-4/3 inline\" />`,\n          jsx: `() => <img className=\"w-10 h-10 flex grid aspect-[4/3] inline\" />`,\n          jsxOutput: `() => <img className=\"size-10  flex grid aspect-4/3 inline\" />`,\n          svelte: `<img class=\"w-10 h-10 flex grid aspect-[4/3] inline\" />`,\n          svelteOutput: `<img class=\"size-10  flex grid aspect-4/3 inline\" />`,\n          vue: `<template><img class=\"w-10 h-10 flex grid aspect-[4/3] inline\" /></template>`,\n          vueOutput: `<template><img class=\"size-10  flex grid aspect-4/3 inline\" /></template>`,\n\n          errors: [\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.multiple, {\n                canonicalClass: \"size-10\",\n                classNames: \"w-10, h-10\"\n              })\n            },\n            {\n              message: values(enforceCanonicalClasses.messages!.single, {\n                canonicalClass: \"aspect-4/3\",\n                className: \"aspect-[4/3]\"\n              })\n            }\n          ],\n\n          files: {\n            \"styles.css\": css`\n              @import \"tailwindcss\";\n            `\n          },\n          options: [{\n            collapse: true,\n            entryPoint: \"styles.css\"\n          }]\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "src/rules/enforce-canonical-classes.ts",
    "content": "import {\n  array,\n  boolean,\n  description,\n  optional,\n  pipe,\n  strictObject,\n  string\n} from \"valibot\";\n\nimport { createGetCanonicalClasses, getCanonicalClasses } from \"better-tailwindcss:tailwindcss/canonical-classes.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { getCachedRegex } from \"better-tailwindcss:utils/regex.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { deduplicateClasses, splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const enforceCanonicalClasses = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce canonical class names.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-canonical-classes.md\",\n  name: \"enforce-canonical-classes\",\n  recommended: true,\n\n  schema: strictObject({\n    collapse: optional(\n      pipe(\n        boolean(),\n        description(\"Whether to collapse multiple utilities into a single utility if possible.\")\n      ),\n      true\n    ),\n    ignore: optional(\n      pipe(\n        array(\n          string()\n        ),\n        description(\"A list of regular expression patterns for classes that should be ignored by the rule.\")\n      ),\n      []\n    ),\n    logical: optional(\n      pipe(\n        boolean(),\n        description(\"Whether to convert between logical and physical properties when collapsing utilities.\")\n      ),\n      true\n    )\n  }),\n\n  messages: {\n    multiple: \"The classes: \\\"{{ classNames }}\\\" can be simplified to \\\"{{canonicalClass}}\\\".\",\n    single: \"The class: \\\"{{ className }}\\\" can be simplified to \\\"{{canonicalClass}}\\\".\"\n  },\n\n  initialize: ctx => {\n    createGetCanonicalClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\nfunction lintLiterals(ctx: Context<typeof enforceCanonicalClasses>, literals: Literal[]) {\n  const { collapse, ignore, logical, rootFontSize } = ctx.options;\n  const ignoredClassRegexes = ignore.map(ignoredClass => getCachedRegex(ignoredClass));\n\n  for(const literal of literals){\n\n    const classes = splitClasses(literal.content);\n    const uniqueClasses = deduplicateClasses(classes);\n\n    const filteredUniqueClasses = uniqueClasses.filter(className => !ignoredClassRegexes.some(ignoredClassRegex => ignoredClassRegex.test(className)));\n\n    if(filteredUniqueClasses.length === 0){\n      continue;\n    }\n\n    const { canonicalClasses, warnings } = getCanonicalClasses(async(ctx), filteredUniqueClasses, {\n      collapse,\n      logicalToPhysical: logical,\n      rem: rootFontSize\n    });\n\n    lintClasses(ctx, literal, className => {\n      const canonicalClass = canonicalClasses[className];\n\n      if(!canonicalClass){\n        return;\n      }\n\n      if(canonicalClass.input.length > 1){\n        return {\n          data: {\n            canonicalClass: canonicalClasses[className].output,\n            classNames: canonicalClass.input.join(\", \")\n          },\n          fix: className === canonicalClass.input[0]\n            ? canonicalClass.output\n            : \"\",\n          id: \"multiple\",\n          warnings\n        } as const;\n      }\n\n      if(canonicalClass.input.length === 1 && canonicalClass.output !== className){\n        return {\n          data: {\n            canonicalClass: canonicalClasses[className].output,\n            className: canonicalClass.input[0]\n          },\n          fix: canonicalClass.output,\n          id: \"single\",\n          warnings\n        } as const;\n      }\n\n    });\n  }\n}\n"
  },
  {
    "path": "src/rules/enforce-consistent-class-order.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentClassOrder } from \"better-tailwindcss:rules/enforce-consistent-class-order.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version.js\";\n\n\ndescribe(enforceConsistentClassOrder.name, () => {\n\n  it(\"should sort simple class names in string literals by the defined order\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"b a\" />`,\n            angularOutput: `<img class=\"a b\" />`,\n            html: `<img class=\"b a\" />`,\n            htmlOutput: `<img class=\"a b\" />`,\n            jsx: `() => <img class=\"b a\" />`,\n            jsxOutput: `() => <img class=\"a b\" />`,\n            svelte: `<img class=\"b a\" />`,\n            svelteOutput: `<img class=\"a b\" />`,\n            vue: `<template><img class=\"b a\" /></template>`,\n            vueOutput: `<template><img class=\"a b\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img class=\"a b\" />`,\n            angularOutput: `<img class=\"b a\" />`,\n            html: `<img class=\"a b\" />`,\n            htmlOutput: `<img class=\"b a\" />`,\n            jsx: `() => <img class=\"a b\" />`,\n            jsxOutput: `() => <img class=\"b a\" />`,\n            svelte: `<img class=\"a b\" />`,\n            svelteOutput: `<img class=\"b a\" />`,\n            vue: `<template><img class=\"a b\" /></template>`,\n            vueOutput: `<template><img class=\"b a\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"desc\" }]\n          },\n          {\n            angular: `<img class=\"w-full absolute\" />`,\n            angularOutput: `<img class=\"absolute w-full\" />`,\n            html: `<img class=\"w-full absolute\" />`,\n            htmlOutput: `<img class=\"absolute w-full\" />`,\n            jsx: `() => <img class=\"w-full absolute\" />`,\n            jsxOutput: `() => <img class=\"absolute w-full\" />`,\n            svelte: `<img class=\"w-full absolute\" />`,\n            svelteOutput: `<img class=\"absolute w-full\" />`,\n            vue: `<template><img class=\"w-full absolute\" /></template>`,\n            vueOutput: `<template><img class=\"absolute w-full\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"official\" }]\n          }\n        ],\n        valid: [\n          {\n            angular: `<img class=\"a b\" />`,\n            html: `<img class=\"a b\" />`,\n            jsx: `() => <img class=\"a b\" />`,\n            svelte: `<img class=\"a b\" />`,\n            vue: `<template><img class=\"a b\" /></template>`,\n\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img class=\"b a\" />`,\n            html: `img class=\"b a\" />`,\n            jsx: `() => <img class=\"b a\" />`,\n            svelte: `img class=\"b a\" />`,\n            vue: `<template><img class=\"b a\" /></template>`,\n\n            options: [{ order: \"desc\" }]\n          },\n          {\n            angular: `<img class=\"absolute w-full\" />`,\n            html: `<img class=\"absolute w-full\" />`,\n            jsx: `() => <img class=\"absolute w-full\" />`,\n            svelte: `<img class=\"absolute w-full\" />`,\n            vue: `<template><img class=\"absolute w-full\" /></template>`,\n\n            options: [{ order: \"official\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should sort alphabetically in a locale-independent way for `asc` and `desc`\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"py-24 px-12\" />`,\n            angularOutput: `<img class=\"px-12 py-24\" />`,\n            html: `<img class=\"py-24 px-12\" />`,\n            htmlOutput: `<img class=\"px-12 py-24\" />`,\n            jsx: `() => <img class=\"py-24 px-12\" />`,\n            jsxOutput: `() => <img class=\"px-12 py-24\" />`,\n            svelte: `<img class=\"py-24 px-12\" />`,\n            svelteOutput: `<img class=\"px-12 py-24\" />`,\n            vue: `<template><img class=\"py-24 px-12\" /></template>`,\n            vueOutput: `<template><img class=\"px-12 py-24\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img class=\"px-12 py-24\" />`,\n            angularOutput: `<img class=\"py-24 px-12\" />`,\n            html: `<img class=\"px-12 py-24\" />`,\n            htmlOutput: `<img class=\"py-24 px-12\" />`,\n            jsx: `() => <img class=\"px-12 py-24\" />`,\n            jsxOutput: `() => <img class=\"py-24 px-12\" />`,\n            svelte: `<img class=\"px-12 py-24\" />`,\n            svelteOutput: `<img class=\"py-24 px-12\" />`,\n            vue: `<template><img class=\"px-12 py-24\" /></template>`,\n            vueOutput: `<template><img class=\"py-24 px-12\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"desc\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should group all classes with the same variant together\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"hover:text-black focus:text-black dark:text-black focus:text-white hover:text-white dark:text-white\" />`,\n          angularOutput: `<img class=\"hover:text-black hover:text-white focus:text-black focus:text-white dark:text-black dark:text-white\" />`,\n          html: `<img class=\"hover:text-black focus:text-black dark:text-black focus:text-white hover:text-white dark:text-white\" />`,\n          htmlOutput: `<img class=\"hover:text-black hover:text-white focus:text-black focus:text-white dark:text-black dark:text-white\" />`,\n          jsx: `() => <img class=\"hover:text-black focus:text-black dark:text-black focus:text-white hover:text-white dark:text-white\" />`,\n          jsxOutput: `() => <img class=\"hover:text-black hover:text-white focus:text-black focus:text-white dark:text-black dark:text-white\" />`,\n          svelte: `<img class=\"hover:text-black focus:text-black dark:text-black focus:text-white hover:text-white dark:text-white\" />`,\n          svelteOutput: `<img class=\"hover:text-black hover:text-white focus:text-black focus:text-white dark:text-black dark:text-white\" />`,\n          vue: `<template><img class=\"hover:text-black focus:text-black dark:text-black focus:text-white hover:text-white dark:text-white\" /></template>`,\n          vueOutput: `<template><img class=\"hover:text-black hover:text-white focus:text-black focus:text-white dark:text-black dark:text-white\" /></template>`,\n\n          errors: 1,\n          options: [{ order: \"official\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should keep the quotes as they are\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"b a\" />`,\n            angularOutput: `<img class=\"a b\" />`,\n            html: `<img class=\"b a\" />`,\n            htmlOutput: `<img class=\"a b\" />`,\n            jsx: `() => <img class=\"b a\" />`,\n            jsxOutput: `() => <img class=\"a b\" />`,\n            svelte: `<img class=\"b a\" />`,\n            svelteOutput: `<img class=\"a b\" />`,\n            vue: `<template><img class=\"b a\" /></template>`,\n            vueOutput: `<template><img class=\"a b\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img class='b a' />`,\n            angularOutput: `<img class='a b' />`,\n            html: `<img class='b a' />`,\n            htmlOutput: `<img class='a b' />`,\n            jsx: `() => <img class='b a' />`,\n            jsxOutput: `() => <img class='a b' />`,\n            svelte: `<img class='b a' />`,\n            svelteOutput: `<img class='a b' />`,\n            vue: `<template><img class='b a' /></template>`,\n            vueOutput: `<template><img class='a b' /></template>`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            jsx: `() => <img class={\\`b a\\`} />`,\n            jsxOutput: `() => <img class={\\`a b\\`} />`,\n            svelte: `<img class={\\`b a\\`} />`,\n            svelteOutput: `<img class={\\`a b\\`} />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            jsx: `() => <img class={\"b a\"} />`,\n            jsxOutput: `() => <img class={\"a b\"} />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            jsx: `() => <img class={'b a'} />`,\n            jsxOutput: `() => <img class={'a b'} />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep expressions as they are\", () => {\n    lint(enforceConsistentClassOrder, {\n      valid: [\n        {\n          jsx: `() => <img class={true ? \"b a\" : \"c b\"} />`,\n          svelte: `<img class={true ? \"b a\" : \"c b\"} />`\n        }\n      ]\n    });\n  });\n\n  it(\"should keep expressions where they are\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`c a \\${true ? \"e\" : \"f\"} d b \\`} />`,\n          jsxOutput: `() => <img class={\\`a c \\${true ? \"e\" : \"f\"} b d \\`} />`,\n          svelte: `<img class={\\`c a \\${true ? \"e\" : \"f\"} d b \\`} />`,\n          svelteOutput: `<img class={\\`a c \\${true ? \"e\" : \"f\"} b d \\`} />`,\n\n          errors: 2,\n          options: [{ order: \"asc\" }]\n        }\n      ],\n      valid: [\n        {\n          jsx: `() => <img class={\\`a c \\${true ? \"e\" : \"f\"} b \\`} />`,\n          svelte: `<img class={\\`a c \\${true ? \"e\" : \"f\"} b \\`} />`\n        }\n      ]\n    });\n  });\n\n  it(\"should not rip away sticky classes\", () => {\n\n    const expression = \"${true ? ' true ' : ' false '}\";\n\n    const dirty = `c b a${expression}f e d`;\n    const clean = `b c a${expression}f d e`;\n\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirty}\\`} />`,\n          jsxOutput: `() => <img class={\\`${clean}\\`} />`,\n          svelte: `<img class={\\`${dirty}\\`} />`,\n          svelteOutput: `<img class={\\`${clean}\\`} />`,\n\n          errors: 2,\n          options: [{ order: \"asc\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should sort multiline strings but keep the whitespace as it is\", () => {\n    const unsortedMultilineString = `\n      d c\n      b a\n    `;\n\n    const sortedMultilineString = `\n      a b\n      c d\n    `;\n\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${unsortedMultilineString}\" />`,\n            angularOutput: `<img class=\"${sortedMultilineString}\" />`,\n            html: `<img class=\"${unsortedMultilineString}\" />`,\n            htmlOutput: `<img class=\"${sortedMultilineString}\" />`,\n            svelte: `<img class=\"${unsortedMultilineString}\" />`,\n            svelteOutput: `<img class=\"${sortedMultilineString}\" />`,\n            vue: `<template><img class=\"${unsortedMultilineString}\" /></template>`,\n            vueOutput: `<template><img class=\"${sortedMultilineString}\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img class='${unsortedMultilineString}' />`,\n            angularOutput: `<img class='${sortedMultilineString}' />`,\n            html: `<img class='${unsortedMultilineString}' />`,\n            htmlOutput: `<img class='${sortedMultilineString}' />`,\n            svelte: `<img class='${unsortedMultilineString}' />`,\n            svelteOutput: `<img class='${sortedMultilineString}' />`,\n            vue: `<template><img class='${unsortedMultilineString}' /></template>`,\n            vueOutput: `<template><img class='${sortedMultilineString}' /></template>`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          },\n          {\n            jsx: `() => <img class={\\`${unsortedMultilineString}\\`} />`,\n            jsxOutput: `() => <img class={\\`${sortedMultilineString}\\`} />`,\n            svelte: `<img class={\\`${unsortedMultilineString}\\`} />`,\n            svelteOutput: `<img class={\\`${sortedMultilineString}\\`} />`,\n\n            errors: 1,\n            options: [{ order: \"asc\" }]\n          }\n        ],\n        valid: [\n          {\n            angular: `<img class=\"${sortedMultilineString}\" />`,\n            html: `<img class=\"${sortedMultilineString}\" />`,\n            svelte: `<img class=\"${sortedMultilineString}\" />`,\n            vue: `<template><img class=\"${sortedMultilineString}\" /></template>`,\n\n            options: [{ order: \"asc\" }]\n          },\n          {\n            angular: `<img class='${sortedMultilineString}' />`,\n            html: `<img class='${sortedMultilineString}' />`,\n            svelte: `<img class='${sortedMultilineString}' />`,\n            vue: `<template><img class='${sortedMultilineString}' /></template>`,\n\n            options: [{ order: \"asc\" }]\n          },\n          {\n            jsx: `() => <img class={\\`${sortedMultilineString}\\`} />`,\n            svelte: `<img class={\\`${sortedMultilineString}\\`} />`,\n\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should sort in string literals in defined call signature arguments\", () => {\n\n    const dirtyDefined = \"defined('b a d c');\";\n    const cleanDefined = \"defined('a b c d');\";\n    const dirtyUndefined = \"notDefined(\\\"b a d c\\\");\";\n\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            jsx: dirtyDefined,\n            jsxOutput: cleanDefined,\n            svelte: `<script>${dirtyDefined}</script>`,\n            svelteOutput: `<script>${cleanDefined}</script>`,\n            vue: `<script>${dirtyDefined}</script>`,\n            vueOutput: `<script>${cleanDefined}</script>`,\n\n            errors: 1,\n            options: [{ callees: [\"defined\"], order: \"asc\" }]\n          }\n        ],\n        valid: [\n          {\n            jsx: dirtyUndefined,\n            svelte: `<script>${dirtyUndefined}</script>`,\n            vue: `<script>${dirtyUndefined}</script>`,\n\n            options: [{ callees: [\"defined\"], order: \"asc\" }]\n          }\n        ]\n      }\n    );\n\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            jsx: dirtyDefined,\n            jsxOutput: cleanDefined,\n            svelte: `<script>${dirtyDefined}</script>`,\n            svelteOutput: `<script>${cleanDefined}</script>`,\n            vue: `<script>${dirtyDefined}</script>`,\n            vueOutput: `<script>${cleanDefined}</script>`,\n\n            errors: 1,\n            options: [{ callees: [\"defined\"], order: \"asc\" }]\n          }\n        ],\n        valid: [\n          {\n            jsx: dirtyUndefined,\n            svelte: `<script>${dirtyUndefined}</script>`,\n            vue: `<script>${dirtyUndefined}</script>`,\n\n            options: [{ callees: [\"defined\"], order: \"asc\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should sort in call signature arguments in template literals\", () => {\n\n    const dirtyDefined = \"${defined('f e')}\";\n    const cleanDefined = \"${defined('e f')}\";\n    const dirtyUndefined = \"${notDefined('f e')}\";\n\n    const dirtyDefinedMultiline = `\n      b a\n      d c ${dirtyDefined} h g\n      j i\n    `;\n    const cleanDefinedMultiline = `\n      a b\n      c d ${cleanDefined} g h\n      i j\n    `;\n    const dirtyUndefinedMultiline = `\n      b a\n      d c ${dirtyUndefined} h g\n      j i\n    `;\n    const cleanUndefinedMultiline = `\n      a b\n      c d ${dirtyUndefined} g h\n      i j\n    `;\n\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={\\`${dirtyDefinedMultiline}\\`} />`,\n            jsxOutput: `() => <img class={\\`${cleanDefinedMultiline}\\`} />`,\n            svelte: `<img class={\\`${dirtyDefinedMultiline}\\`} />`,\n            svelteOutput: `<img class={\\`${cleanDefinedMultiline}\\`} />`,\n\n            errors: 3,\n            options: [{ callees: [\"defined\"], order: \"asc\" }]\n          },\n          {\n            jsx: `() => <img class={\\`${dirtyUndefinedMultiline}\\`} />`,\n            jsxOutput: `() => <img class={\\`${cleanUndefinedMultiline}\\`} />`,\n            svelte: `<img class={\\`${dirtyUndefinedMultiline}\\`} />`,\n            svelteOutput: `<img class={\\`${cleanUndefinedMultiline}\\`} />`,\n\n            errors: 2,\n            options: [{ callees: [\"defined\"], order: \"asc\" }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should sort in matching variable declarations\", () => {\n\n    const dirtyDefined = \"const defined = \\\"b a\\\";\";\n    const cleanDefined = \"const defined = \\\"a b\\\";\";\n    const dirtyUndefined = \"const notDefined = \\\"b a\\\";\";\n\n    const dirtyMultiline = `const defined = \\`\n      b a\n      d c\n    \\`;`;\n\n    const cleanMultiline = `const defined = \\`\n      a b\n      c d\n    \\`;`;\n\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            jsx: dirtyDefined,\n            jsxOutput: cleanDefined,\n            svelte: `<script>${dirtyDefined}</script>`,\n            svelteOutput: `<script>${cleanDefined}</script>`,\n            vue: `<script>${dirtyDefined}</script>`,\n            vueOutput: `<script>${cleanDefined}</script>`,\n\n            errors: 1,\n            options: [{ order: \"asc\", variables: [\"defined\"] }]\n          },\n          {\n            jsx: dirtyMultiline,\n            jsxOutput: cleanMultiline,\n            svelte: `<script>${dirtyMultiline}</script>`,\n            svelteOutput: `<script>${cleanMultiline}</script>`,\n            vue: `<script>${dirtyMultiline}</script>`,\n            vueOutput: `<script>${cleanMultiline}</script>`,\n\n            errors: 1,\n            options: [{ order: \"asc\", variables: [\"defined\"] }]\n          }\n        ],\n        valid: [\n          {\n            jsx: dirtyUndefined,\n            svelte: `<script>${dirtyUndefined}</script>`,\n            vue: `<script>${dirtyUndefined}</script>`,\n\n            options: [{ order: \"asc\" }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should sort simple class names in tagged template literals\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            jsx: \"defined`b a`\",\n            jsxOutput: \"defined`a b`\",\n            svelte: \"<script>defined`b a`</script>\",\n            svelteOutput: \"<script>defined`a b`</script>\",\n            vue: \"defined`b a`\",\n            vueOutput: \"defined`a b`\",\n\n            errors: 1,\n            options: [{ order: \"asc\", tags: [\"defined\"] }]\n          }\n        ],\n        valid: [\n          {\n            jsx: \"defined`a b`\",\n            svelte: \"<script>defined`a b`</script>\",\n            vue: \"defined`a b`\",\n\n            options: [{ order: \"asc\", tags: [\"defined\"] }]\n          }\n        ]\n      }\n    );\n  });\n\n\n  it(\"should group variants together in the `strict` sorting order\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"hover:text-black text-black hover:dark:text-black\" />`,\n          angularOutput: `<img class=\"text-black hover:text-black hover:dark:text-black\" />`,\n          html: `<img class=\"hover:text-black text-black hover:dark:text-black\" />`,\n          htmlOutput: `<img class=\"text-black hover:text-black hover:dark:text-black\" />`,\n          jsx: `() => <img class=\"hover:text-black text-black hover:dark:text-black\" />`,\n          jsxOutput: `() => <img class=\"text-black hover:text-black hover:dark:text-black\" />`,\n          svelte: `<img class=\"hover:text-black text-black hover:dark:text-black\" />`,\n          svelteOutput: `<img class=\"text-black hover:text-black hover:dark:text-black\" />`,\n          vue: `<template><img class=\"hover:text-black text-black hover:dark:text-black\" /></template>`,\n          vueOutput: `<template><img class=\"text-black hover:text-black hover:dark:text-black\" /></template>`,\n\n          errors: 1,\n          options: [{ order: \"strict\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should group arbitrary variants together in the `strict` sorting order\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          jsx: `<img class=\"data-[attr=a]:*:text-black text-black data-[attr=a]:text-black\" />`,\n          jsxOutput: `<img class=\"text-black data-[attr=a]:text-black data-[attr=a]:*:text-black\" />`,\n\n          errors: 1,\n          options: [{ order: \"strict\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should sort arbitrary variants last in the `strict` sorting order\", () => {\n    lint(enforceConsistentClassOrder, {\n      invalid: [\n        {\n          jsx: `<img class=\"data-[attr=a]:*:text-black data-[attr=a]:text-black text-black md:dark:text-black md:text-black\" />`,\n          jsxOutput: `<img class=\"text-black md:text-black md:dark:text-black data-[attr=a]:text-black data-[attr=a]:*:text-black\" />`,\n\n          errors: 1,\n          options: [{ order: \"strict\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should sort unknown classes to the start by default\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex unknown\" />`,\n            angularOutput: `<img class=\"unknown flex\" />`,\n            html: `<img class=\"flex unknown\" />`,\n            htmlOutput: `<img class=\"unknown flex\" />`,\n            jsx: `() => <img class=\"flex unknown\" />`,\n            jsxOutput: `() => <img class=\"unknown flex\" />`,\n            svelte: `<img class=\"flex unknown\" />`,\n            svelteOutput: `<img class=\"unknown flex\" />`,\n            vue: `<template><img class=\"flex unknown\" /></template>`,\n            vueOutput: `<template><img class=\"unknown flex\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should sort unknown classes alphabetically in a locale-independent way\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex cy-24 cx-12\" />`,\n            angularOutput: `<img class=\"cx-12 cy-24 flex\" />`,\n            html: `<img class=\"flex cy-24 cx-12\" />`,\n            htmlOutput: `<img class=\"cx-12 cy-24 flex\" />`,\n            jsx: `() => <img class=\"flex cy-24 cx-12\" />`,\n            jsxOutput: `() => <img class=\"cx-12 cy-24 flex\" />`,\n            svelte: `<img class=\"flex cy-24 cx-12\" />`,\n            svelteOutput: `<img class=\"cx-12 cy-24 flex\" />`,\n            vue: `<template><img class=\"flex cy-24 cx-12\" /></template>`,\n            vueOutput: `<template><img class=\"cx-12 cy-24 flex\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"official\", unknownClassOrder: \"asc\", unknownClassPosition: \"start\" }]\n          },\n          {\n            angular: `<img class=\"flex cx-12 cy-24\" />`,\n            angularOutput: `<img class=\"flex cy-24 cx-12\" />`,\n            html: `<img class=\"flex cx-12 cy-24\" />`,\n            htmlOutput: `<img class=\"flex cy-24 cx-12\" />`,\n            jsx: `() => <img class=\"flex cx-12 cy-24\" />`,\n            jsxOutput: `() => <img class=\"flex cy-24 cx-12\" />`,\n            svelte: `<img class=\"flex cx-12 cy-24\" />`,\n            svelteOutput: `<img class=\"flex cy-24 cx-12\" />`,\n            vue: `<template><img class=\"flex cx-12 cy-24\" /></template>`,\n            vueOutput: `<template><img class=\"flex cy-24 cx-12\" /></template>`,\n\n            errors: 1,\n            options: [{ order: \"official\", unknownClassOrder: \"desc\", unknownClassPosition: \"end\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should sort component classes to the start by default in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex custom-component\" />`,\n            angularOutput: `<img class=\"custom-component flex\" />`,\n            html: `<img class=\"flex custom-component\" />`,\n            htmlOutput: `<img class=\"custom-component flex\" />`,\n            jsx: `() => <img class=\"flex custom-component\" />`,\n            jsxOutput: `() => <img class=\"custom-component flex\" />`,\n            svelte: `<img class=\"flex custom-component\" />`,\n            svelteOutput: `<img class=\"custom-component flex\" />`,\n            vue: `<template><img class=\"flex custom-component\" /></template>`,\n            vueOutput: `<template><img class=\"custom-component flex\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                \n                @layer components {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should sort component classes alphabetically in a locale-independent way in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex cy-24 cx-12\" />`,\n            angularOutput: `<img class=\"cx-12 cy-24 flex\" />`,\n            html: `<img class=\"flex cy-24 cx-12\" />`,\n            htmlOutput: `<img class=\"cx-12 cy-24 flex\" />`,\n            jsx: `() => <img class=\"flex cy-24 cx-12\" />`,\n            jsxOutput: `() => <img class=\"cx-12 cy-24 flex\" />`,\n            svelte: `<img class=\"flex cy-24 cx-12\" />`,\n            svelteOutput: `<img class=\"cx-12 cy-24 flex\" />`,\n            vue: `<template><img class=\"flex cy-24 cx-12\" /></template>`,\n            vueOutput: `<template><img class=\"cx-12 cy-24 flex\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                \n                @layer components {\n                  .cx-12, .cy-24 {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              componentClassOrder: \"asc\",\n              componentClassPosition: \"start\",\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"flex cx-12 cy-24\" />`,\n            angularOutput: `<img class=\"flex cy-24 cx-12\" />`,\n            html: `<img class=\"flex cx-12 cy-24\" />`,\n            htmlOutput: `<img class=\"flex cy-24 cx-12\" />`,\n            jsx: `() => <img class=\"flex cx-12 cy-24\" />`,\n            jsxOutput: `() => <img class=\"flex cy-24 cx-12\" />`,\n            svelte: `<img class=\"flex cx-12 cy-24\" />`,\n            svelteOutput: `<img class=\"flex cy-24 cx-12\" />`,\n            vue: `<template><img class=\"flex cx-12 cy-24\" /></template>`,\n            vueOutput: `<template><img class=\"flex cy-24 cx-12\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                \n                @layer components {\n                  .cx-12, .cy-24 {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              componentClassOrder: \"desc\",\n              componentClassPosition: \"end\",\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should differentiate between custom component classes and unknown classes in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"g d h flex c b i a h-full k e w-full f j\" />`,\n            angularOutput: `<img class=\"a b c d e f flex h-full w-full k j i h g\" />`,\n            html: `<img class=\"g d h flex c b i a h-full k e w-full f j\" />`,\n            htmlOutput: `<img class=\"a b c d e f flex h-full w-full k j i h g\" />`,\n            jsx: `() => <img class=\"g d h flex c b i a h-full k e w-full f j\" />`,\n            jsxOutput: `() => <img class=\"a b c d e f flex h-full w-full k j i h g\" />`,\n            svelte: `<img class=\"g d h flex c b i a h-full k e w-full f j\" />`,\n            svelteOutput: `<img class=\"a b c d e f flex h-full w-full k j i h g\" />`,\n            vue: `<template><img class=\"g d h flex c b i a h-full k e w-full f j\" /></template>`,\n            vueOutput: `<template><img class=\"a b c d e f flex h-full w-full k j i h g\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                \n                @layer components {\n                  .a, .b, .c, .d, .e, .f {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              componentClassOrder: \"asc\",\n              componentClassPosition: \"start\",\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\",\n              unknownClassOrder: \"desc\",\n              unknownClassPosition: \"end\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should be possible to move both custom component classes and unknown classes to the end and preserve the order in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentClassOrder,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"g d h flex c b i a w-full k e h-full f j\" />`,\n            angularOutput: `<img class=\"flex h-full w-full g h i k j d c b a e f\" />`,\n            html: `<img class=\"g d h flex c b i a w-full k e h-full f j\" />`,\n            htmlOutput: `<img class=\"flex h-full w-full g h i k j d c b a e f\" />`,\n            jsx: `() => <img class=\"g d h flex c b i a w-full k e h-full f j\" />`,\n            jsxOutput: `() => <img class=\"flex h-full w-full g h i k j d c b a e f\" />`,\n            svelte: `<img class=\"g d h flex c b i a w-full k e h-full f j\" />`,\n            svelteOutput: `<img class=\"flex h-full w-full g h i k j d c b a e f\" />`,\n            vue: `<template><img class=\"g d h flex c b i a w-full k e h-full f j\" /></template>`,\n            vueOutput: `<template><img class=\"flex h-full w-full g h i k j d c b a e f\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                \n                @layer components {\n                  .a, .b, .c, .d, .e, .f {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              componentClassOrder: \"preserve\",\n              componentClassPosition: \"end\",\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\",\n              unknownClassOrder: \"preserve\",\n              unknownClassPosition: \"end\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n});\n"
  },
  {
    "path": "src/rules/enforce-consistent-class-order.ts",
    "content": "import { description, literal, optional, pipe, strictObject, union } from \"valibot\";\n\nimport { createGetClassOrder, getClassOrder } from \"better-tailwindcss:tailwindcss/class-order.js\";\nimport {\n  createGetCustomComponentClasses,\n  getCustomComponentClasses\n} from \"better-tailwindcss:tailwindcss/custom-component-classes.js\";\nimport { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { escapeNestedQuotes } from \"better-tailwindcss:utils/quotes.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { display, splitClasses, splitWhitespaces } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { DissectedClass } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const enforceConsistentClassOrder = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce a consistent order for tailwind classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-consistent-class-order.md\",\n  name: \"enforce-consistent-class-order\",\n  recommended: true,\n\n  messages: {\n    order: \"Incorrect class order. Expected\\n\\n{{ notSorted }}\\n\\nto be\\n\\n{{ sorted }}\"\n  },\n\n  schema: strictObject({\n    componentClassOrder: optional(\n      pipe(\n        union([\n          literal(\"asc\"),\n          literal(\"desc\"),\n          literal(\"preserve\")\n        ]),\n        description(\"Defines how component classes should be ordered among themselves.\")\n      ),\n      \"preserve\"\n    ),\n    componentClassPosition: optional(\n      pipe(\n        union([\n          literal(\"start\"),\n          literal(\"end\")\n        ]),\n        description(\"Defines where component classes should be placed in relation to the whole string literal.\")\n      ),\n      \"start\"\n    ),\n    order: optional(\n      pipe(\n        union([\n          literal(\"asc\"),\n          literal(\"desc\"),\n          literal(\"official\"),\n          literal(\"strict\")\n        ]),\n        description(\"The algorithm to use when sorting classes.\")\n      ),\n      \"official\"\n    ),\n    unknownClassOrder: optional(\n      pipe(\n        union([\n          literal(\"asc\"),\n          literal(\"desc\"),\n          literal(\"preserve\")\n        ]),\n        description(\"Defines how component classes should be ordered among themselves.\")\n      ),\n      \"preserve\"\n    ),\n    unknownClassPosition: optional(\n      pipe(\n        union([\n          literal(\"start\"),\n          literal(\"end\")\n        ]),\n        description(\"Defines where component classes should be placed in relation to the whole string literal.\")\n      ),\n      \"start\"\n    )\n  }),\n\n  initialize: ctx => {\n    const { detectComponentClasses } = ctx.options;\n\n    createGetClassOrder(ctx);\n    createGetDissectedClasses(ctx);\n\n    if(detectComponentClasses){\n      createGetCustomComponentClasses(ctx);\n    }\n  },\n\n  lintLiterals: (ctx, literals) => {\n\n    const { messageStyle } = ctx.options;\n\n    for(const literal of literals){\n\n      const classChunks = splitClasses(literal.content);\n\n      if(classChunks.length <= 1){\n        continue;\n      }\n\n      const whitespaceChunks = splitWhitespaces(literal.content);\n\n      const unsortableClasses: [string, string] = [\"\", \"\"];\n\n      // remove sticky classes\n      if(literal.closingBraces && whitespaceChunks[0] === \"\"){\n        whitespaceChunks.shift();\n        unsortableClasses[0] = classChunks.shift() ?? \"\";\n      }\n      if(literal.openingBraces && whitespaceChunks[whitespaceChunks.length - 1] === \"\"){\n        whitespaceChunks.pop();\n        unsortableClasses[1] = classChunks.pop() ?? \"\";\n      }\n\n      const [sortedClassChunks, warnings] = sortClassNames(ctx, classChunks);\n\n      const classes: string[] = [];\n\n      for(let i = 0; i < Math.max(sortedClassChunks.length, whitespaceChunks.length); i++){\n        whitespaceChunks[i] && classes.push(whitespaceChunks[i]);\n        sortedClassChunks[i] && classes.push(sortedClassChunks[i]);\n      }\n\n      const escapedClasses = escapeNestedQuotes(\n        [\n          unsortableClasses[0],\n          ...classes,\n          unsortableClasses[1]\n        ].join(\"\"),\n        literal.openingQuote ?? literal.closingQuote ?? \"`\"\n      );\n\n      const fixedClasses = [\n        literal.openingQuote ?? \"\",\n        literal.isInterpolated && literal.closingBraces ? literal.closingBraces : \"\",\n        escapedClasses,\n        literal.isInterpolated && literal.openingBraces ? literal.openingBraces : \"\",\n        literal.closingQuote ?? \"\"\n      ].join(\"\");\n\n      if(literal.raw === fixedClasses){\n        continue;\n      }\n\n      ctx.report({\n        data: {\n          notSorted: display(messageStyle, literal.raw),\n          sorted: display(messageStyle, fixedClasses)\n        },\n        fix: fixedClasses,\n        id: \"order\",\n        range: literal.range,\n        warnings\n      });\n    }\n  }\n});\n\n\nfunction sortClassNames(ctx: Context<typeof enforceConsistentClassOrder>, classes: string[]): [classes: string[], warnings?: (Warning | undefined)[]] {\n\n  const { componentClassOrder, componentClassPosition, order, unknownClassOrder, unknownClassPosition } = ctx.options;\n\n  if(order === \"asc\"){\n    return [classes.toSorted((a, b) => compareClasses(a, b))];\n  }\n\n  if(order === \"desc\"){\n    return [classes.toSorted((a, b) => compareClasses(b, a))];\n  }\n\n  if(classes.length <= 1){\n    return [classes];\n  }\n\n  const { classOrder, warnings } = getClassOrder(async(ctx), classes);\n  const { detectComponentClasses } = ctx.options;\n  const customComponentClasses = detectComponentClasses\n    ? getCustomComponentClasses(async(ctx)).customComponentClasses\n    : [];\n\n  const officiallySortedClasses = classOrder\n    .toSorted((a, b) => {\n      const [classA, aIndex] = a;\n      const [classB, bIndex] = b;\n\n      const componentClassSorting = getCustomOrder(\n        componentClassPosition,\n        componentClassOrder,\n        classA,\n        classB,\n        className => customComponentClasses.includes(className)\n      );\n\n      if(componentClassSorting !== undefined){\n        return componentClassSorting;\n      }\n\n      const unknownClassSorting = getCustomOrder(\n        unknownClassPosition,\n        unknownClassOrder,\n        classA,\n        classB,\n        className => {\n          return (\n            (classA === className && aIndex === null || classB === className && bIndex === null) &&\n            !customComponentClasses.includes(className)\n          );\n        }\n      );\n\n      if(unknownClassSorting !== undefined){\n        return unknownClassSorting;\n      }\n\n      if(aIndex === bIndex){ return 0; }\n      if(aIndex === null){ return -1; }\n      if(bIndex === null){ return +1; }\n      return +(aIndex - bIndex > 0n) - +(aIndex - bIndex < 0n);\n    })\n    .map(([className]) => className);\n\n  if(order === \"official\"){\n    return [officiallySortedClasses, warnings];\n  }\n\n  const { dissectedClasses } = getDissectedClasses(async(ctx), classes);\n\n  const variantMap: VariantMap = {};\n\n  for(const originalClass in dissectedClasses){\n    const dissectedClass = dissectedClasses[originalClass];\n\n    // parse variants manually for custom component classes\n    const variants = dissectedClass.variants ?? originalClass.match(/^(.*):/)?.[1]?.split(\":\") ?? [];\n\n    variants.unshift(\"\");\n\n    for(let v = 0, variantMapLevel = variantMap; v < variants.length; v++){\n      const isLastVariant = v === variants.length - 1;\n\n      variantMapLevel[variants[v]] ??= {\n        dissectedClasses: [],\n        nested: {}\n      };\n\n      if(isLastVariant){\n        variantMapLevel[variants[v]].dissectedClasses.push(dissectedClass);\n        continue;\n      }\n\n      variantMapLevel = variantMapLevel[variants[v]].nested;\n\n    }\n  }\n\n  const strictOrder = getStrictOrder(variantMap);\n\n  return [strictOrder, warnings];\n\n}\n\n\ntype VariantMap = {\n  [variant: string]: {\n    dissectedClasses: DissectedClass[];\n    nested: VariantMap;\n  };\n};\n\nfunction getStrictOrder(variantMap: VariantMap): string[] {\n  const orderedClasses: string[] = [];\n\n  const orderedVariants = Object.keys(variantMap).sort((a, b) => {\n    const aIsArbitrary = isArbitrary(a);\n    const bIsArbitrary = isArbitrary(b);\n\n    // sort arbitrary variants last\n    if(aIsArbitrary && !bIsArbitrary){ return +1; }\n    if(!aIsArbitrary && bIsArbitrary){ return -1; }\n\n    return 0;\n  });\n\n  for(let v = 0; v < orderedVariants.length; v++){\n    const variant = orderedVariants[v];\n    const nextVariant = orderedVariants[v + 1];\n\n    const variantIsArbitrary = isArbitrary(variant);\n    const nextVariantIsArbitrary = isArbitrary(nextVariant);\n\n    const { dissectedClasses, nested } = variantMap[variant];\n\n    orderedClasses.push(...dissectedClasses.map(dissectedClass => dissectedClass.className));\n\n    if(dissectedClasses.length > 0 || !variantIsArbitrary && nextVariantIsArbitrary){\n      orderedClasses.push(...getStrictOrder(nested));\n    }\n  }\n\n  for(let v = 0; v < orderedVariants.length; v++){\n    const variant = orderedVariants[v];\n    const nextVariant = orderedVariants[v + 1];\n\n    const variantIsArbitrary = isArbitrary(variant);\n    const nextVariantIsArbitrary = isArbitrary(nextVariant);\n\n    const { dissectedClasses, nested } = variantMap[variant];\n\n    if(!(dissectedClasses.length > 0 || !variantIsArbitrary && nextVariantIsArbitrary)){\n      orderedClasses.push(...getStrictOrder(nested));\n    }\n  }\n\n  return orderedClasses;\n\n}\n\nfunction getCustomOrder(position: \"end\" | \"start\", order: \"asc\" | \"desc\" | \"preserve\", classA: string, classB: string, isCustomClass: (className: string) => boolean): number | undefined {\n  const aIsCustomClass = isCustomClass(classA);\n  const bIsCustomClass = isCustomClass(classB);\n\n  if(position === \"start\"){\n    if(aIsCustomClass && !bIsCustomClass){ return -1; }\n    if(!aIsCustomClass && bIsCustomClass){ return +1; }\n\n    if(aIsCustomClass && bIsCustomClass){\n      if(order === \"asc\"){\n        return compareClasses(classA, classB);\n      }\n      if(order === \"desc\"){\n        return compareClasses(classB, classA);\n      }\n      return 0;\n    }\n  }\n  if(position === \"end\"){\n    if(aIsCustomClass && !bIsCustomClass){ return +1; }\n    if(!aIsCustomClass && bIsCustomClass){ return -1; }\n\n    if(aIsCustomClass && bIsCustomClass){\n      if(order === \"asc\"){\n        return compareClasses(classA, classB);\n      }\n      if(order === \"desc\"){\n        return compareClasses(classB, classA);\n      }\n      return 0;\n    }\n  }\n\n}\n\nfunction compareClasses(classA: string, classB: string): number {\n  if(classA === classB){ return 0; }\n  return classA < classB ? -1 : +1;\n}\n\nfunction isArbitrary(variant?: string): boolean {\n  if(!variant){\n    return false;\n  }\n\n  return variant.includes(\"[\") && variant.includes(\"]\");\n}\n"
  },
  {
    "path": "src/rules/enforce-consistent-important-position.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentImportantPosition } from \"better-tailwindcss:rules/enforce-consistent-important-position.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css, ts } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\n\n\ndescribe(enforceConsistentImportantPosition.name, () => {\n\n  it(`should move the important modifier correct position`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!w-4\" />`,\n            angularOutput: `<img class=\"w-4!\" />`,\n            html: `<img class=\"!w-4\" />`,\n            htmlOutput: `<img class=\"w-4!\" />`,\n            jsx: `() => <img class=\"!w-4\" />`,\n            jsxOutput: `() => <img class=\"w-4!\" />`,\n            svelte: `<img class=\"!w-4\" />`,\n            svelteOutput: `<img class=\"w-4!\" />`,\n            vue: `<template><img class=\"!w-4\" /></template>`,\n            vueOutput: `<template><img class=\"w-4!\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"w-4!\" />`,\n            angularOutput: `<img class=\"!w-4\" />`,\n            html: `<img class=\"w-4!\" />`,\n            htmlOutput: `<img class=\"!w-4\" />`,\n            jsx: `() => <img class=\"w-4!\" />`,\n            jsxOutput: `() => <img class=\"!w-4\" />`,\n            svelte: `<img class=\"w-4!\" />`,\n            svelteOutput: `<img class=\"!w-4\" />`,\n            vue: `<template><img class=\"w-4!\" /></template>`,\n            vueOutput: `<template><img class=\"!w-4\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should handle classes with variants correctly`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"hover:!text-red-500\" />`,\n            angularOutput: `<img class=\"hover:text-red-500!\" />`,\n            html: `<img class=\"hover:!text-red-500\" />`,\n            htmlOutput: `<img class=\"hover:text-red-500!\" />`,\n            jsx: `() => <img class=\"hover:!text-red-500\" />`,\n            jsxOutput: `() => <img class=\"hover:text-red-500!\" />`,\n            svelte: `<img class=\"hover:!text-red-500\" />`,\n            svelteOutput: `<img class=\"hover:text-red-500!\" />`,\n            vue: `<template><img class=\"hover:!text-red-500\" /></template>`,\n            vueOutput: `<template><img class=\"hover:text-red-500!\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"hover:text-red-500!\" />`,\n            angularOutput: `<img class=\"hover:!text-red-500\" />`,\n            html: `<img class=\"hover:text-red-500!\" />`,\n            htmlOutput: `<img class=\"hover:!text-red-500\" />`,\n            jsx: `() => <img class=\"hover:text-red-500!\" />`,\n            jsxOutput: `() => <img class=\"hover:!text-red-500\" />`,\n            svelte: `<img class=\"hover:text-red-500!\" />`,\n            svelteOutput: `<img class=\"hover:!text-red-500\" />`,\n            vue: `<template><img class=\"hover:text-red-500!\" /></template>`,\n            vueOutput: `<template><img class=\"hover:!text-red-500\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should handle multiple variants`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"sm:hover:!bg-blue-500\" />`,\n            angularOutput: `<img class=\"sm:hover:bg-blue-500!\" />`,\n            html: `<img class=\"sm:hover:!bg-blue-500\" />`,\n            htmlOutput: `<img class=\"sm:hover:bg-blue-500!\" />`,\n            jsx: `() => <img class=\"sm:hover:!bg-blue-500\" />`,\n            jsxOutput: `() => <img class=\"sm:hover:bg-blue-500!\" />`,\n            svelte: `<img class=\"sm:hover:!bg-blue-500\" />`,\n            svelteOutput: `<img class=\"sm:hover:bg-blue-500!\" />`,\n            vue: `<template><img class=\"sm:hover:!bg-blue-500\" /></template>`,\n            vueOutput: `<template><img class=\"sm:hover:bg-blue-500!\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"sm:hover:bg-blue-500!\" />`,\n            angularOutput: `<img class=\"sm:hover:!bg-blue-500\" />`,\n            html: `<img class=\"sm:hover:bg-blue-500!\" />`,\n            htmlOutput: `<img class=\"sm:hover:!bg-blue-500\" />`,\n            jsx: `() => <img class=\"sm:hover:bg-blue-500!\" />`,\n            jsxOutput: `() => <img class=\"sm:hover:!bg-blue-500\" />`,\n            svelte: `<img class=\"sm:hover:bg-blue-500!\" />`,\n            svelteOutput: `<img class=\"sm:hover:!bg-blue-500\" />`,\n            vue: `<template><img class=\"sm:hover:bg-blue-500!\" /></template>`,\n            vueOutput: `<template><img class=\"sm:hover:!bg-blue-500\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should handle multiple classes with mixed important positions`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!w-4 hover:text-red-500! normal-class\" />`,\n            angularOutput: `<img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            html: `<img class=\"!w-4 hover:text-red-500! normal-class\" />`,\n            htmlOutput: `<img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            jsx: `() => <img class=\"!w-4 hover:text-red-500! normal-class\" />`,\n            jsxOutput: `() => <img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            svelte: `<img class=\"!w-4 hover:text-red-500! normal-class\" />`,\n            svelteOutput: `<img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            vue: `<template><img class=\"!w-4 hover:text-red-500! normal-class\" /></template>`,\n            vueOutput: `<template><img class=\"w-4! hover:text-red-500! normal-class\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"w-4! hover:!text-red-500 normal-class\" />`,\n            angularOutput: `<img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            html: `<img class=\"w-4! hover:!text-red-500 normal-class\" />`,\n            htmlOutput: `<img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            jsx: `() => <img class=\"w-4! hover:!text-red-500 normal-class\" />`,\n            jsxOutput: `() => <img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            svelte: `<img class=\"w-4! hover:!text-red-500 normal-class\" />`,\n            svelteOutput: `<img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            vue: `<template><img class=\"w-4! hover:!text-red-500 normal-class\" /></template>`,\n            vueOutput: `<template><img class=\"!w-4 hover:!text-red-500 normal-class\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should handle arbitrary values correctly`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!w-[100px]\" />`,\n            angularOutput: `<img class=\"w-[100px]!\" />`,\n            html: `<img class=\"!w-[100px]\" />`,\n            htmlOutput: `<img class=\"w-[100px]!\" />`,\n            jsx: `() => <img class=\"!w-[100px]\" />`,\n            jsxOutput: `() => <img class=\"w-[100px]!\" />`,\n            svelte: `<img class=\"!w-[100px]\" />`,\n            svelteOutput: `<img class=\"w-[100px]!\" />`,\n            vue: `<template><img class=\"!w-[100px]\" /></template>`,\n            vueOutput: `<template><img class=\"w-[100px]!\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"w-[100px]!\" />`,\n            angularOutput: `<img class=\"!w-[100px]\" />`,\n            html: `<img class=\"w-[100px]!\" />`,\n            htmlOutput: `<img class=\"!w-[100px]\" />`,\n            jsx: `() => <img class=\"w-[100px]!\" />`,\n            jsxOutput: `() => <img class=\"!w-[100px]\" />`,\n            svelte: `<img class=\"w-[100px]!\" />`,\n            svelteOutput: `<img class=\"!w-[100px]\" />`,\n            vue: `<template><img class=\"w-[100px]!\" /></template>`,\n            vueOutput: `<template><img class=\"!w-[100px]\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should not report errors for correctly positioned important modifiers`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            html: `<img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            jsx: `() => <img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            svelte: `<img class=\"w-4! hover:text-red-500! normal-class\" />`,\n            vue: `<template><img class=\"w-4! hover:text-red-500! normal-class\" /></template>`,\n\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            html: `<img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            jsx: `() => <img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            svelte: `<img class=\"!w-4 hover:!text-red-500 normal-class\" />`,\n            vue: `<template><img class=\"!w-4 hover:!text-red-500 normal-class\" /></template>`,\n\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should not report errors for classes without important modifiers`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            html: `<img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            jsx: `() => <img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            svelte: `<img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            vue: `<template><img class=\"w-4 hover:text-red-500 normal-class\" /></template>`,\n\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            html: `<img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            jsx: `() => <img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            svelte: `<img class=\"w-4 hover:text-red-500 normal-class\" />`,\n            vue: `<template><img class=\"w-4 hover:text-red-500 normal-class\" /></template>`,\n\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(`should use \"recommended\" as default position when no option is provided in tailwind >= 4`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!w-4\" />`,\n            angularOutput: `<img class=\"w-4!\" />`,\n            html: `<img class=\"!w-4\" />`,\n            htmlOutput: `<img class=\"w-4!\" />`,\n            jsx: `() => <img class=\"!w-4\" />`,\n            jsxOutput: `() => <img class=\"w-4!\" />`,\n            svelte: `<img class=\"!w-4\" />`,\n            svelteOutput: `<img class=\"w-4!\" />`,\n            vue: `<template><img class=\"!w-4\" /></template>`,\n            vueOutput: `<template><img class=\"w-4!\" /></template>`,\n\n            errors: 1\n            // No options provided - should default to \"recommended\"\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(`should use \"legacy\" as default position when no option is provided in tailwind <= 3`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"w-4!\" />`,\n            angularOutput: `<img class=\"!w-4\" />`,\n            html: `<img class=\"w-4!\" />`,\n            htmlOutput: `<img class=\"!w-4\" />`,\n            jsx: `() => <img class=\"w-4!\" />`,\n            jsxOutput: `() => <img class=\"!w-4\" />`,\n            svelte: `<img class=\"w-4!\" />`,\n            svelteOutput: `<img class=\"!w-4\" />`,\n            vue: `<template><img class=\"w-4!\" /></template>`,\n            vueOutput: `<template><img class=\"!w-4\" /></template>`,\n\n            errors: 1\n            // No options provided - should default to \"legacy\"\n          }\n        ]\n      }\n    );\n  });\n\n  it(`should keep modifiers in the correct position when changing the important position`, () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!bg-red-500/50\" />`,\n            angularOutput: `<img class=\"bg-red-500/50!\" />`,\n            html: `<img class=\"!bg-red-500/50\" />`,\n            htmlOutput: `<img class=\"bg-red-500/50!\" />`,\n            jsx: `() => <img class=\"!bg-red-500/50\" />`,\n            jsxOutput: `() => <img class=\"bg-red-500/50!\" />`,\n            svelte: `<img class=\"!bg-red-500/50\" />`,\n            svelteOutput: `<img class=\"bg-red-500/50!\" />`,\n            vue: `<template><img class=\"!bg-red-500/50\" /></template>`,\n            vueOutput: `<template><img class=\"bg-red-500/50!\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"recommended\" }]\n          },\n          {\n            angular: `<img class=\"bg-red-500/50!\" />`,\n            angularOutput: `<img class=\"!bg-red-500/50\" />`,\n            html: `<img class=\"bg-red-500/50!\" />`,\n            htmlOutput: `<img class=\"!bg-red-500/50\" />`,\n            jsx: `() => <img class=\"bg-red-500/50!\" />`,\n            jsxOutput: `() => <img class=\"!bg-red-500/50\" />`,\n            svelte: `<img class=\"bg-red-500/50!\" />`,\n            svelteOutput: `<img class=\"!bg-red-500/50\" />`,\n            vue: `<template><img class=\"bg-red-500/50!\" /></template>`,\n            vueOutput: `<template><img class=\"!bg-red-500/50\" /></template>`,\n\n            errors: 1,\n            options: [{ position: \"legacy\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with prefixed tailwind classes in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw-w-4!\" />`,\n            angularOutput: `<img class=\"!tw-w-4\" />`,\n            html: `<img class=\"tw-w-4!\" />`,\n            htmlOutput: `<img class=\"!tw-w-4\" />`,\n            jsx: `() => <img class=\"tw-w-4!\" />`,\n            jsxOutput: `() => <img class=\"!tw-w-4\" />`,\n            svelte: `<img class=\"tw-w-4!\" />`,\n            svelteOutput: `<img class=\"!tw-w-4\" />`,\n            vue: `<template><img class=\"tw-w-4!\" /></template>`,\n            vueOutput: `<template><img class=\"!tw-w-4\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              position: \"legacy\",\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          },\n          {\n            angular: `<img class=\"hover:tw-w-4!\" />`,\n            angularOutput: `<img class=\"hover:!tw-w-4\" />`,\n            html: `<img class=\"hover:tw-w-4!\" />`,\n            htmlOutput: `<img class=\"hover:!tw-w-4\" />`,\n            jsx: `() => <img class=\"hover:tw-w-4!\" />`,\n            jsxOutput: `() => <img class=\"hover:!tw-w-4\" />`,\n            svelte: `<img class=\"hover:tw-w-4!\" />`,\n            svelteOutput: `<img class=\"hover:!tw-w-4\" />`,\n            vue: `<template><img class=\"hover:tw-w-4!\" /></template>`,\n            vueOutput: `<template><img class=\"hover:!tw-w-4\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              position: \"legacy\",\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with prefixed tailwind classes in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentImportantPosition,\n\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw:!w-4\" />`,\n            angularOutput: `<img class=\"tw:w-4!\" />`,\n            html: `<img class=\"tw:!w-4\" />`,\n            htmlOutput: `<img class=\"tw:w-4!\" />`,\n            jsx: `() => <img class=\"tw:!w-4\" />`,\n            jsxOutput: `() => <img class=\"tw:w-4!\" />`,\n            svelte: `<img class=\"tw:!w-4\" />`,\n            svelteOutput: `<img class=\"tw:w-4!\" />`,\n            vue: `<template><img class=\"tw:!w-4\" /></template>`,\n            vueOutput: `<template><img class=\"tw:w-4!\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw:hover:!w-4\" />`,\n            angularOutput: `<img class=\"tw:hover:w-4!\" />`,\n            html: `<img class=\"tw:hover:!w-4\" />`,\n            htmlOutput: `<img class=\"tw:hover:w-4!\" />`,\n            jsx: `() => <img class=\"tw:hover:!w-4\" />`,\n            jsxOutput: `() => <img class=\"tw:hover:w-4!\" />`,\n            svelte: `<img class=\"tw:hover:!w-4\" />`,\n            svelteOutput: `<img class=\"tw:hover:w-4!\" />`,\n            vue: `<template><img class=\"tw:hover:!w-4\" /></template>`,\n            vueOutput: `<template><img class=\"tw:hover:w-4!\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n});\n"
  },
  {
    "path": "src/rules/enforce-consistent-important-position.ts",
    "content": "import { description, literal, optional, pipe, strictObject, union } from \"valibot\";\n\nimport { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { buildClass } from \"better-tailwindcss:utils/class.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\n\nexport const enforceConsistentImportantPosition = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce consistent important position for classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-consistent-important-position.md\",\n  name: \"enforce-consistent-important-position\",\n  recommended: false,\n\n  messages: {\n    position: \"Incorrect important position. '{{ className }}' should be '{{ fix }}'.\"\n  },\n\n  schema: strictObject({\n    position: optional(\n      pipe(\n        union([\n          literal(\"legacy\"),\n          literal(\"recommended\")\n        ]),\n        description(\"Preferred position for important classes. 'legacy' places the important modifier (!) at the start of the class name, 'recommended' places it at the end.\")\n      )\n    )\n  }),\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n  },\n\n  lintLiterals(ctx, literals) {\n\n    const { position: configuredPosition } = ctx.options;\n\n    const position = configuredPosition ?? (\n      ctx.version.major >= 4\n        ? \"recommended\"\n        : \"legacy\"\n    );\n\n    for(const literal of literals){\n\n      const classes = splitClasses(literal.content);\n\n      const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);\n\n      lintClasses(ctx, literal, (className, index, after) => {\n        const dissectedClass = dissectedClasses[className];\n\n        if(!dissectedClass){\n          return;\n        }\n\n        const [importantAtStart, importantAtEnd] = dissectedClass.important;\n\n        if(\n          !importantAtStart && !importantAtEnd ||\n          position === \"legacy\" && importantAtStart ||\n          position === \"recommended\" && importantAtEnd\n        ){\n          return;\n        }\n\n        if(ctx.version.major <= 3 && position === \"recommended\"){\n          warnings.push({\n            option: \"position\",\n            title: `The \"${position}\" position is not supported in Tailwind CSS v3`,\n            url: `${ctx.docs}#position`\n          });\n        }\n\n        const fix = position === \"recommended\"\n          ? buildClass(ctx, { ...dissectedClass, important: [false, true] })\n          : buildClass(ctx, { ...dissectedClass, important: [true, false] });\n\n        return {\n          data: { className, fix },\n          fix,\n          id: \"position\",\n          warnings\n        } as const;\n      });\n\n    }\n  }\n});\n"
  },
  {
    "path": "src/rules/enforce-consistent-line-wrapping.test.ts",
    "content": "import eslintParserHTML from \"@html-eslint/parser\";\nimport { ESLint } from \"eslint\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { enforceConsistentLineWrapping } from \"better-tailwindcss:rules/enforce-consistent-line-wrapping.js\";\nimport { eslint } from \"better-tailwindcss:tests/utils/eslint.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { prettier } from \"better-tailwindcss:tests/utils/prettier.js\";\nimport { css, dedent, jsx, ts } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\nimport { MatcherType } from \"better-tailwindcss:types/rule.js\";\n\nimport eslintPluginBetterTailwindcss from \"../configs/config.js\";\n\n\ndescribe(enforceConsistentLineWrapping.name, () => {\n\n  it(\"should not wrap empty strings\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            angular: `<img class=\"\" />`,\n            html: `<img class=\"\" />`,\n            jsx: `() => <img class=\"\" />`,\n            svelte: `<img class=\"\" />`,\n            vue: `<template><img class=\"\" /></template>`\n          },\n          {\n            angular: `<img class='' />`,\n            html: `<img class='' />`,\n            jsx: `() => <img class='' />`,\n            svelte: `<img class='' />`,\n            vue: `<template><img class='' /></template>`\n          },\n          {\n            jsx: `() => <img class={\"\"} />`,\n            svelte: `<img class={\"\"} />`\n          },\n          {\n            jsx: `() => <img class={''} />`,\n            svelte: `<img class={''} />`\n          },\n          {\n            jsx: `() => <img class={\\`\\`} />`,\n            svelte: `<img class={\\`\\`} />`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not wrap short lines\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            angular: `<img class=\"a b c\" />`,\n            html: `<img class=\"a b c\" />`,\n            jsx: `() => <img class=\"a b c\" />`,\n            svelte: `<img class=\"a b c\" />`,\n            vue: `<template><img class=\"a b c\" /></template>`\n          },\n          {\n            angular: `<img class='a b c' />`,\n            html: `<img class='a b c' />`,\n            jsx: `() => <img class='a b c' />`,\n            svelte: `<img class='a b c' />`,\n            vue: `<template><img class='a b c' /></template>`\n          },\n          {\n            jsx: `() => <img class={\"a b c\"} />`,\n            svelte: `<img class={\"a b c\"} />`\n          },\n          {\n            jsx: `() => <img class={'a b c'} />`,\n            svelte: `<img class={'a b c'} />`\n          },\n          {\n            jsx: `() => <img class={\\`a b c\\`} />`,\n            svelte: `<img class={\\`a b c\\`} />`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should collapse unnecessarily wrapped short lines\", () => {\n\n    const dirty = dedent`\n      a b\n    `;\n\n    const clean = \"a b\";\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${dirty}\" />`,\n            angularOutput: `<img class=\"${clean}\" />`,\n            html: `<img class=\"${dirty}\" />`,\n            htmlOutput: `<img class=\"${clean}\" />`,\n            jsx: `() => <img class={\\`${dirty}\\`} />`,\n            jsxOutput: `() => <img class={\\`${clean}\\`} />`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n            vue: `<template><img class=\"${dirty}\" /></template>`,\n            vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n            errors: 1,\n            options: [{ printWidth: 60 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not clean up whitespace in single line strings\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            angular: `<img class=\"  a  b  c  \" />`,\n            html: `<img class=\"  a  b  c  \" />`,\n            jsx: `() => <img class=\"  a  b  c  \" />`,\n            svelte: `<img class=\"  a  b  c  \" />`,\n            vue: `<template><img class=\"  a  b  c  \" /></template>`,\n\n            options: [{ printWidth: 60 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should wrap and not collapse short lines containing expressions\", () => {\n\n    const expression = \"${true ? 'true' : 'false'}\";\n\n    const incorrect = dedent`\n      a ${expression}\n    `;\n\n    const correct = dedent`\n      a\n      ${expression}\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"{{\\`${incorrect}\\`}}\" />`,\n            angularOutput: `<img class=\"{{\\`${correct}\\`}}\" />`,\n            jsx: `() => <img class={\\`${incorrect}\\`} />`,\n            jsxOutput: `() => <img class={\\`${correct}\\`} />`,\n            svelte: `<img class={\\`${incorrect}\\`} />`,\n            svelteOutput: `<img class={\\`${correct}\\`} />`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should include previous characters to decide if lines should be wrapped\", () => {\n\n    const dirty = \"this string literal is exactly 54 characters in length\";\n    const clean = dedent`\n      this string literal is exactly 54 characters in length\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${dirty}\" />`,\n            angularOutput: `<img class=\"${clean}\" />`,\n            html: `<img class=\"${dirty}\" />`,\n            htmlOutput: `<img class=\"${clean}\" />`,\n            jsx: `() => <img class=\"${dirty}\" />`,\n            jsxOutput: `() => <img class=\"${clean}\" />`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n            vue: `<template><img class=\"${dirty}\" /></template>`,\n            vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n            errors: 1,\n            options: [{ printWidth: 60 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not insert an empty line if the first class is already too long\", () => {\n\n    const dirty = \"this-string-literal-is-exactly-54-characters-in-length\";\n    const clean = dedent`\n      this-string-literal-is-exactly-54-characters-in-length\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${dirty}\" />`,\n            angularOutput: `<img class=\"${clean}\" />`,\n            html: `<img class=\"${dirty}\" />`,\n            htmlOutput: `<img class=\"${clean}\" />`,\n            jsx: `() => <img class=\"${dirty}\" />`,\n            jsxOutput: `() => <img class=\"${clean}\" />`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n            vue: `<template><img class=\"${dirty}\" /></template>`,\n            vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n            errors: 1,\n            options: [{ printWidth: 50 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should disable the `printWidth` limit when set to `0`\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            angular: `<img class=\"this string literal is longer than 80 characters and would be wrapped using the default printWidth\" />`,\n            html: `<img class=\"this string literal is longer than 80 characters and would be wrapped using the default printWidth\" />`,\n            jsx: `() => <img class=\"this string literal is longer than 80 characters and would be wrapped using the default printWidth\" />`,\n            svelte: `<img class=\"this string literal is longer than 80 characters and would be wrapped using the default printWidth\" />`,\n            vue: `<template><img class=\"this string literal is longer than 80 characters and would be wrapped using the default printWidth\" /></template>`,\n\n            options: [{ printWidth: 0 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should change the quotes in defined call signatures to backticks\", () => {\n\n    const dirtyDefined = \"defined('a b c d e f g h')\";\n\n    const cleanDefined = dedent`defined(\\`\n      a b c\n      d e f\n      g h\n    \\`)`;\n\n    const dirtyUndefined = \"notDefined('a b c d e f g h')\";\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={${dirtyDefined}} />`,\n            jsxOutput: `() => <img class={${cleanDefined}} />`,\n            svelte: `<img class={${dirtyDefined}} />`,\n            svelteOutput: `<img class={${cleanDefined}} />`,\n\n            errors: 1,\n            options: [{ callees: [\"defined\"], classesPerLine: 3, indent: 2 }]\n          }\n        ],\n        valid: [\n          {\n            jsx: `() => <img class={${dirtyUndefined}} />`,\n            svelte: `<img class={${dirtyUndefined}} />`,\n\n            options: [{ callees: [\"defined\"], classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={${dirtyDefined}} />`,\n            jsxOutput: `() => <img class={${cleanDefined}} />`,\n            svelte: `<img class={${dirtyDefined}} />`,\n            svelteOutput: `<img class={${cleanDefined}} />`,\n\n            errors: 1,\n            options: [{ callees: [\"defined\"], classesPerLine: 3, indent: 2 }]\n          }\n        ],\n        valid: [\n          {\n            jsx: `() => <img class={${dirtyUndefined}} />`,\n            svelte: `<img class={${dirtyUndefined}} />`,\n\n            options: [{ callees: [\"defined\"], classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should change the quotes in defined variables to backticks\", () => {\n\n    const dirtyDefined = `const defined = \"a b c d e f g h\"`;\n\n    const cleanDefined = dedent`const defined = \\`\n      a b c\n      d e f\n      g h\n    \\``;\n\n    const dirtyUndefined = `const notDefined = \"a b c d e f g h\"`;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: dirtyDefined,\n            jsxOutput: cleanDefined,\n            svelte: `<script>${dirtyDefined}</script>`,\n            svelteOutput: `<script>${cleanDefined}</script>`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: 2, variables: [\"defined\"] }]\n          }\n        ],\n        valid: [\n          {\n            jsx: dirtyUndefined,\n            svelte: `<script>${dirtyUndefined}</script>`,\n\n            options: [{ classesPerLine: 3, indent: 2, variables: [\"defined\"] }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should change the quotes in conditional expressions to backticks\", () => {\n\n    const dirtyConditionalExpression = `true ? \"1 2 3 4 5 6 7 8\" : \"9 10 11 12 13 14 15 16\"`;\n    const cleanConditionalExpression = `true ? \\`\\n  1 2 3\\n  4 5 6\\n  7 8\\n\\` : \\`\\n  9 10 11\\n  12 13 14\\n  15 16\\n\\``;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={${dirtyConditionalExpression}} />`,\n            jsxOutput: `() => <img class={${cleanConditionalExpression}} />`,\n            svelte: `<img class={${dirtyConditionalExpression}} />`,\n            svelteOutput: `<img class={${cleanConditionalExpression}} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should change the quotes in logical expressions to backticks\", () => {\n\n    const dirtyLogicalExpression = `true && \"1 2 3 4 5 6 7 8\"`;\n    const cleanLogicalExpression = `true && \\`\\n  1 2 3\\n  4 5 6\\n  7 8\\n\\``;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={${dirtyLogicalExpression}} />`,\n            jsxOutput: `() => <img class={${cleanLogicalExpression}} />`,\n            svelte: `<img class={${dirtyLogicalExpression}} />`,\n            svelteOutput: `<img class={${cleanLogicalExpression}} />`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should change the quotes in arrays to backticks\", () => {\n\n    const dirtyArray = `[\"1 2 3 4 5 6 7 8\", \"9 10 11 12 13 14 15 16\"]`;\n    const cleanArray = `[\\`\\n  1 2 3\\n  4 5 6\\n  7 8\\n\\`, \\`\\n  9 10 11\\n  12 13 14\\n  15 16\\n\\`]`;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={${dirtyArray}} />`,\n            jsxOutput: `() => <img class={${cleanArray}} />`,\n            svelte: `<img class={${dirtyArray}} />`,\n            svelteOutput: `<img class={${cleanArray}} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should always preserve the original quotes in attributes\", () => {\n\n    const singleLine = \" a b c d e f g h \";\n    const multipleLines = dedent`\n      a b c\n      d e f\n      g h\n    `;\n\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          angular: `<img class=\"${singleLine}\" />`,\n          angularOutput: `<img class=\"${multipleLines}\" />`,\n          html: `<img class=\"${singleLine}\" />`,\n          htmlOutput: `<img class=\"${multipleLines}\" />`,\n          jsx: `() => <img class=\"${singleLine}\" />`,\n          jsxOutput: `() => <img class=\"${multipleLines}\" />`,\n          svelte: `<img class=\"${singleLine}\" />`,\n          svelteOutput: `<img class=\"${multipleLines}\" />`,\n          vue: `<template><img class=\"${singleLine}\" /></template>`,\n          vueOutput: `<template><img class=\"${multipleLines}\" /></template>`,\n\n          errors: 1,\n          options: [{ classesPerLine: 3, indent: 2 }]\n        },\n        {\n          angular: `<img class='${singleLine}' />`,\n          angularOutput: `<img class='${multipleLines}' />`,\n          html: `<img class='${singleLine}' />`,\n          htmlOutput: `<img class='${multipleLines}' />`,\n          jsx: `() => <img class='${singleLine}' />`,\n          jsxOutput: `() => <img class='${multipleLines}' />`,\n          svelte: `<img class='${singleLine}' />`,\n          svelteOutput: `<img class='${multipleLines}' />`,\n          vue: `<template><img class='${singleLine}' /></template>`,\n          vueOutput: `<template><img class='${multipleLines}' /></template>`,\n\n          errors: 1,\n          options: [{ classesPerLine: 3, indent: 2 }]\n        }\n      ]\n    });\n  });\n\n  it(\"should change the quotes to backticks in attribute expressions\", () => {\n\n    const singleLine = \" a b c d e f g h \";\n    const multipleLines = dedent`\n      a b c\n      d e f\n      g h\n    `;\n\n    lint(enforceConsistentLineWrapping, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${singleLine}\\`} />`,\n          jsxOutput: `() => <img class={\\`${multipleLines}\\`} />`,\n          svelte: `<img class={\\`${singleLine}\\`} />`,\n          svelteOutput: `<img class={\\`${multipleLines}\\`} />`,\n\n          errors: 1,\n          options: [{ classesPerLine: 3, indent: 2 }]\n        },\n        {\n          jsx: `() => <img class={\"${singleLine}\"} />`,\n          jsxOutput: `() => <img class={\\`${multipleLines}\\`} />`,\n          svelte: `<img class={\"${singleLine}\"} />`,\n          svelteOutput: `<img class={\\`${multipleLines}\\`} />`,\n\n          errors: 1,\n          options: [{ classesPerLine: 3, indent: 2 }]\n        },\n        {\n          jsx: `() => <img class={'${singleLine}'} />`,\n          jsxOutput: `() => <img class={\\`${multipleLines}\\`} />`,\n          svelte: `<img class={'${singleLine}'} />`,\n          svelteOutput: `<img class={\\`${multipleLines}\\`} />`,\n\n          errors: 1,\n          options: [{ classesPerLine: 3, indent: 2 }]\n        }\n      ]\n    });\n  });\n\n  it(\"should wrap expressions correctly\", () => {\n\n    const expression = \"${true ? 'true' : 'false'}\";\n\n    const singleLineWithExpressionAtBeginning = `${expression} a b c d e f g h `;\n    const multilineWithExpressionAtBeginning = dedent`\n      ${expression}\n      a b c\n      d e f\n      g h\n    `;\n\n    const singleLineWithExpressionInCenter = `a b c ${expression} d e f g h `;\n    const multilineWithExpressionInCenter = dedent`\n      a b c\n      ${expression}\n      d e f\n      g h\n    `;\n\n    const singleLineWithExpressionAtEnd = `a b c d e f g h ${expression}`;\n    const multilineWithExpressionAtEnd = dedent`\n      a b c\n      d e f\n      g h\n      ${expression}\n    `;\n\n    const singleLineWithClassesAroundExpression = `a b ${expression} c d e f g h `;\n    const multilineWithClassesAroundExpression = dedent`\n      a b\n      ${expression}\n      c d e f\n      g h\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionAtBeginning}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionAtBeginning}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionAtBeginning}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionAtBeginning}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          },\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionInCenter}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionInCenter}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionInCenter}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionInCenter}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          },\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionAtEnd}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionAtEnd}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionAtEnd}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionAtEnd}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          },\n          {\n            jsx: `() => <img class={\\`${singleLineWithClassesAroundExpression}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithClassesAroundExpression}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithClassesAroundExpression}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithClassesAroundExpression}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 4, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should not place expressions on a new line when the expression is not surrounded by a space\", () => {\n\n    const expression = \"${true ? 'true' : 'false'}\";\n\n    const singleLineWithExpressionAtBeginningWithStickyClassAtEnd = `${expression}a b c d e f g h `;\n    const multilineWithExpressionAtBeginningWithStickyClassAtEnd = dedent`\n      ${expression}a\n      b c d\n      e f g\n      h\n    `;\n\n    const singleLineWithExpressionInCenterWithStickyClassAtBeginning = `a b c${expression} d e f g h `;\n    const multilineWithExpressionInCenterWithStickyClassAtBeginning = dedent`\n      a b\n      c${expression}\n      d e f\n      g h\n    `;\n\n    const singleLineWithExpressionInCenterWithStickyClassAtEnd = `a b c ${expression}d e f g h `;\n    const multilineWithExpressionInCenterWithStickyClassAtEnd = dedent`\n      a b c\n      ${expression}d\n      e f g\n      h\n    `;\n\n    const singleLineWithExpressionAtEndWithStickyClassAtBeginning = `a b c d e f g h${expression}`;\n    const multilineWithExpressionAtEndWithStickyClassAtBeginning = dedent`\n      a b c\n      d e f\n      g\n      h${expression}\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionAtBeginningWithStickyClassAtEnd}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionAtBeginningWithStickyClassAtEnd}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionAtBeginningWithStickyClassAtEnd}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionAtBeginningWithStickyClassAtEnd}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          },\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionInCenterWithStickyClassAtBeginning}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionInCenterWithStickyClassAtBeginning}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionInCenterWithStickyClassAtBeginning}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionInCenterWithStickyClassAtBeginning}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          },\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionInCenterWithStickyClassAtEnd}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionInCenterWithStickyClassAtEnd}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionInCenterWithStickyClassAtEnd}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionInCenterWithStickyClassAtEnd}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          },\n          {\n            jsx: `() => <img class={\\`${singleLineWithExpressionAtEndWithStickyClassAtBeginning}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineWithExpressionAtEndWithStickyClassAtBeginning}\\`} />`,\n            svelte: `<img class={\\`${singleLineWithExpressionAtEndWithStickyClassAtBeginning}\\`} />`,\n            svelteOutput: `<img class={\\`${multilineWithExpressionAtEndWithStickyClassAtBeginning}\\`} />`,\n\n            errors: 2,\n            options: [{ classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should not add an unnecessary new line after a sticky class\", () => {\n\n    const expression = \"${true ? 'true' : 'false'}\";\n\n    const multilineWithWithStickyClassAtEnd = dedent`\n      ${expression}a\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            jsx: `() => <img class={\\`${multilineWithWithStickyClassAtEnd}\\`} />`,\n            svelte: `<img class={\\`${multilineWithWithStickyClassAtEnd}\\`} />`,\n\n            options: [{ classesPerLine: 3, indent: 2 }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should wrap string literals in variable declarations\", () => {\n\n    const dirtyDefined = \"const defined = 'a b c d e f g h';\";\n    const dirtyUndefined = \"const notDefined = 'a b c d e f g h';\";\n    const cleanDefined = dedent`const defined = \\`\n      a b c\n      d e f\n      g h\n    \\`;`;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: dirtyDefined,\n            jsxOutput: cleanDefined,\n            svelte: `<script>${dirtyDefined}</script>`,\n            svelteOutput: `<script>${cleanDefined}</script>`,\n            vue: `<script>${dirtyDefined}</script>`,\n            vueOutput: `<script>${cleanDefined}</script>`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: 2, variables: [\"defined\"] }]\n          }\n        ],\n        valid: [\n          {\n            jsx: dirtyUndefined,\n            svelte: `<script>${dirtyUndefined}</script>`,\n\n            options: [{ classesPerLine: 3, indent: 2, variables: [\"defined\"] }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should never wrap in an object key\", () => {\n\n    const dirtyObject = dedent`const obj = {\n      \"a b c d e f g h\": \"a b c d e f g h\"\n    };`;\n    const cleanObject = dedent`const obj = {\n      \"a b c d e f g h\": \\`\n        a b c\n        d e f\n        g h\n      \\`\n    };`;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: dirtyObject,\n            jsxOutput: cleanObject,\n            svelte: `<script>${dirtyObject}</script>`,\n            svelteOutput: `<script>${cleanObject}</script>`,\n            vue: `<script>${dirtyObject}</script>`,\n            vueOutput: `<script>${cleanObject}</script>`,\n\n            errors: 1,\n            options: [{\n              classesPerLine: 3,\n              indent: 2,\n              variables: [\n                [\"obj\", [{ match: MatcherType.ObjectKey }, { match: MatcherType.ObjectValue }]]\n              ]\n            }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should be possible to change the lineBreakStyle to windows\", () => {\n\n    const dirty = \" a b c d e f g h \";\n    const clean = \"\\r\\n  a b c\\r\\n  d e f\\r\\n  g h\\r\\n\";\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${dirty}\" />`,\n            angularOutput: `<img class=\"${clean}\" />`,\n            html: `<img class=\"${dirty}\" />`,\n            htmlOutput: `<img class=\"${clean}\" />`,\n            jsx: `() => <img class=\"${dirty}\" />`,\n            jsxOutput: `() => <img class=\"${clean}\" />`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n            vue: `<template><img class=\"${dirty}\" /></template>`,\n            vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: 2, lineBreakStyle: \"windows\" }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it(\"should be possible to change the indentation style to tabs\", () => {\n\n    const dirty = \" a b c d e f g h \";\n    const clean = \"\\n\\ta b c\\n\\td e f\\n\\tg h\\n\";\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${dirty}\" />`,\n            angularOutput: `<img class=\"${clean}\" />`,\n            html: `<img class=\"${dirty}\" />`,\n            htmlOutput: `<img class=\"${clean}\" />`,\n            jsx: `() => <img class=\"${dirty}\" />;`,\n            jsxOutput: `() => <img class=\"${clean}\" />;`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n            vue: `<template><img class=\"${dirty}\" /></template>`,\n            vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: \"tab\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should use tabWidth when checking printWidth\", () => {\n\n    const dirty = \"a b c d\";\n    const clean = \"\\n\\ta b c\\n\\td\\n\";\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class=\"${dirty}\" />`,\n            jsxOutput: `() => <img class=\"${clean}\" />`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n\n            errors: 1,\n            options: [{ classesPerLine: 0, indent: \"tab\", printWidth: 10, tabWidth: 4 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should default tabWidth to 1 when it is not configured\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class=\"a b c d\" />`,\n            jsxOutput: `() => <img class=\"\\n\\ta b c d\\n\" />`,\n            svelte: `<img class=\"a b c d\" />`,\n            svelteOutput: `<img class=\"\\n\\ta b c d\\n\" />`,\n\n            errors: 1,\n            options: [{ classesPerLine: 0, indent: \"tab\", printWidth: 10 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not apply tabWidth when indentation uses spaces\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class=\"a b c d\" />`,\n            jsxOutput: `() => <img class=\"\\n  a b c d\\n\" />`,\n            svelte: `<img class=\"a b c d\" />`,\n            svelteOutput: `<img class=\"\\n  a b c d\\n\" />`,\n\n            errors: 1,\n            options: [{ classesPerLine: 0, indent: 2, printWidth: 10, tabWidth: 8 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should still ignore printWidth when it is set to 0 even with tabWidth\", () => {\n\n    const dirty = \"a b c d\";\n    const clean = \"\\n\\ta b c\\n\\td\\n\";\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: `() => <img class=\"${dirty}\" />`,\n            jsxOutput: `() => <img class=\"${clean}\" />`,\n            svelte: `<img class=\"${dirty}\" />`,\n            svelteOutput: `<img class=\"${clean}\" />`,\n\n            errors: 1,\n            options: [{ classesPerLine: 3, indent: \"tab\", printWidth: 0, tabWidth: 4 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should warn if `lineBreakStyle` is likely misconfigured\", async () => {\n    {\n\n      const linter = new ESLint({\n        overrideConfig: [{\n          languageOptions: {\n            parser: eslintParserHTML\n          },\n          plugins: {\n            \"better-tailwindcss\": eslintPluginBetterTailwindcss\n          },\n          rules: {\n            \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n              classesPerLine: 3,\n              indent: 2,\n              lineBreakStyle: \"unix\"\n            }]\n          }\n        }]\n      });\n\n      const [result] = await linter.lintText(\"<img class=\\\"\\r\\n  a b c d\\r\\n\\\" />\");\n      const { message } = result.messages.find(message => message.ruleId === \"better-tailwindcss/enforce-consistent-line-wrapping\")!;\n\n      expect(message).toContain(\"Inconsistent line endings detected\");\n      expect(message).toContain(\"Option `lineBreakStyle` may be misconfigured.\");\n      expect(message).toContain(`${enforceConsistentLineWrapping.rule.meta.docs.url}#linebreakstyle`);\n    }\n    {\n      const linter = new ESLint({\n        overrideConfig: [{\n          languageOptions: {\n            parser: eslintParserHTML\n          },\n          plugins: {\n            \"better-tailwindcss\": eslintPluginBetterTailwindcss\n          },\n          rules: {\n            \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n              classesPerLine: 3,\n              indent: 2,\n              lineBreakStyle: \"windows\"\n            }]\n          }\n        }]\n      });\n\n      const [result] = await linter.lintText(\"<img class=\\\"\\n  a b c d\\n\\\" />\");\n      const { message } = result.messages.find(message => message.ruleId === \"better-tailwindcss/enforce-consistent-line-wrapping\")!;\n\n      expect(message).toContain(\"Inconsistent line endings detected\");\n      expect(message).toContain(\"Option `lineBreakStyle` may be misconfigured.\");\n      expect(message).toContain(`${enforceConsistentLineWrapping.rule.meta.docs.url}#linebreakstyle`);\n    }\n  });\n\n  it(\"should warn if `indent` is likely misconfigured\", async () => {\n    const linter = new ESLint({\n      overrideConfig: [{\n        languageOptions: {\n          parser: eslintParserHTML\n        },\n        plugins: {\n          \"better-tailwindcss\": eslintPluginBetterTailwindcss\n        },\n        rules: {\n          \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n            classesPerLine: 3,\n            indent: 2\n          }]\n        }\n      }]\n    });\n\n    const [result] = await linter.lintText(\"<img class=\\\"\\n\\ta b c d\\n\\\" />\");\n    const { message } = result.messages.find(message => message.ruleId === \"better-tailwindcss/enforce-consistent-line-wrapping\")!;\n\n    expect(message).toContain(\"Inconsistent indentation detected\");\n    expect(message).toContain(\"Option `indent` may be misconfigured.\");\n    expect(message).toContain(`${enforceConsistentLineWrapping.rule.meta.docs.url}#indent`);\n  });\n\n  it(\"should not warn for double spaces between classes\", async () => {\n    const linter = new ESLint({\n      overrideConfig: [{\n        languageOptions: {\n          parser: eslintParserHTML\n        },\n        plugins: {\n          \"better-tailwindcss\": eslintPluginBetterTailwindcss\n        },\n        rules: {\n          \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n            classesPerLine: 3,\n            indent: \"tab\"\n          }]\n        }\n      }]\n    });\n\n    const [result] = await linter.lintText(\"<img class=\\\"a  b c d\\\" />\");\n    const { message } = result.messages.find(message => message.ruleId === \"better-tailwindcss/enforce-consistent-line-wrapping\")!;\n\n    expect(message).not.toContain(\"Inconsistent indentation detected\");\n    expect(message).not.toContain(\"Option `indent` may be misconfigured.\");\n  });\n\n  it(\"should not warn for leading spaces in single-line class strings\", async () => {\n    const linter = new ESLint({\n      overrideConfig: [{\n        languageOptions: {\n          parser: eslintParserHTML\n        },\n        plugins: {\n          \"better-tailwindcss\": eslintPluginBetterTailwindcss\n        },\n        rules: {\n          \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n            classesPerLine: 3,\n            indent: \"tab\"\n          }]\n        }\n      }]\n    });\n\n    const [result] = await linter.lintText(\"<img class=\\\" a b c d\\\" />\");\n    const { message } = result.messages.find(message => message.ruleId === \"better-tailwindcss/enforce-consistent-line-wrapping\")!;\n\n    expect(message).not.toContain(\"Inconsistent indentation detected\");\n    expect(message).not.toContain(\"Option `indent` may be misconfigured.\");\n  });\n\n  // #52\n  it(\"should wrap expressions even if `group` is set to `never`\", () => {\n    const expression = \"${true ? 'b' : 'c'}\";\n\n    const correct = dedent`\n      a\n      ${expression}\n      d\n    `;\n\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            jsx: `() => <img class={\\`${correct}\\`} />`,\n            svelte: `<img class={\\`${correct}\\`} />`,\n\n            options: [{ group: \"never\", indent: 2 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should be possible to change group separation by emptyLines\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            angularOutput: `<img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            html: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            htmlOutput: `<img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            jsx: `() => <img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            jsxOutput: `() => <img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            svelte: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            svelteOutput: `<img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            vue: `<template><img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" /></template>`,\n            vueOutput: `<template><img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" /></template>`,\n\n            errors: 1,\n            options: [{ group: \"emptyLine\", indent: 2 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should be possible to change group separation to emptyLine\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            angularOutput: `<img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            html: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            htmlOutput: `<img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            jsx: `() => <img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            jsxOutput: `() => <img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            svelte: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            svelteOutput: `<img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" />`,\n            vue: `<template><img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" /></template>`,\n            vueOutput: `<template><img class=\"\\n  a b c\\n\\n  g-1:a g-1:b\\n\\n  g-2:a g-2:b\\n\" /></template>`,\n\n            errors: 1,\n            options: [{ group: \"emptyLine\", indent: 2 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should be wrap groups according to preferSingleLine\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            angularOutput: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            html: `<img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            htmlOutput: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            jsx: `() => <img class={\\`\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\\`} />`,\n            jsxOutput: `() => <img class={\\`a b c g-1:a g-1:b g-2:a g-2:b\\`} />`,\n            svelte: `<img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            svelteOutput: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            vue: `<template><img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" /></template>`,\n            vueOutput: `<template><img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" /></template>`,\n\n            errors: 1,\n            options: [{ indent: 2, preferSingleLine: true }]\n          },\n          {\n            angular: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            angularOutput: `<img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            html: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            htmlOutput: `<img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            jsx: `() => <img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            jsxOutput: `() => <img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            svelte: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            svelteOutput: `<img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" />`,\n            vue: `<template><img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" /></template>`,\n            vueOutput: `<template><img class=\"\\n  a b c\\n  g-1:a g-1:b\\n  g-2:a g-2:b\\n\" /></template>`,\n\n            errors: 1,\n            options: [{ classesPerLine: 6, indent: 2, preferSingleLine: true, printWidth: 0 }]\n          }\n        ],\n        valid: [\n          {\n            angular: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            html: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            jsx: `() => <img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            svelte: `<img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" />`,\n            vue: `<template><img class=\"a b c g-1:a g-1:b g-2:a g-2:b\" /></template>`,\n\n            options: [{ indent: 2, preferSingleLine: true }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should still start on a new line when `group` is set to `never` except if `preferSingleLine` is enabled\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            angular: `<img class=\"\\n  a b hover:c\\n\" />`,\n            html: `<img class=\"\\n  a b hover:c\\n\" />`,\n            jsx: `() => <img class=\"\\n  a b hover:c\\n\" />`,\n            svelte: `<img class=\"\\n  a b hover:c\\n\" />`,\n            vue: `<template><img class=\"\\n  a b hover:c\\n\" /></template>`,\n\n            options: [{ group: \"never\", preferSingleLine: false, printWidth: 100 }]\n          },\n          {\n            angular: `<img class=\"a b hover:c\" />`,\n            html: `<img class=\"a b hover:c\" />`,\n            jsx: `() => <img class=\"a b hover:c\" />`,\n            svelte: `<img class=\"a b hover:c\" />`,\n            vue: `<template><img class=\"a b hover:c\" /></template>`,\n\n            options: [{ group: \"never\", preferSingleLine: true, printWidth: 100 }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should remove duplicate classes in string literals in defined tagged template literals\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            jsx: \"defined` a b c d e f g `\",\n            jsxOutput: \"defined`\\n  a b c\\n  d e f\\n  g\\n`\",\n            svelte: \"<script>defined` a b c d e f g`</script>\",\n            svelteOutput: \"<script>defined`\\n  a b c\\n  d e f\\n  g\\n`</script>\",\n            vue: \"<script>defined` a b c d e f g`</script>\",\n            vueOutput: \"<script>defined`\\n  a b c\\n  d e f\\n  g\\n`</script>\",\n\n            errors: 1,\n            options: [{\n              classesPerLine: 3,\n              indent: 2,\n              tags: [\"defined\"]\n            }]\n          }\n        ],\n        valid: [\n          {\n            jsx: \"notDefined` a b c d e f g`\",\n            svelte: \"<script>notDefined` a b c d e f g`</script>\",\n            vue: \"notDefined` a b c d e f g`\",\n\n            options: [{\n              classesPerLine: 3,\n              indent: 2,\n              tags: [\"defined\"]\n            }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should ignore prefixed variants in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw-a tw-b hover:tw-c focus:tw-d\" />`,\n            angularOutput: `<img class=\"\\n  tw-a tw-b\\n  hover:tw-c\\n  focus:tw-d\\n\" />`,\n            html: `<img class=\"tw-a tw-b hover:tw-c focus:tw-d\" />`,\n            htmlOutput: `<img class=\"\\n  tw-a tw-b\\n  hover:tw-c\\n  focus:tw-d\\n\" />`,\n            jsx: `() => <img class=\"tw-a tw-b hover:tw-c focus:tw-d\" />`,\n            jsxOutput: `() => <img class=\"\\n  tw-a tw-b\\n  hover:tw-c\\n  focus:tw-d\\n\" />`,\n            svelte: `<img class=\"tw-a tw-b hover:tw-c focus:tw-d\" />`,\n            svelteOutput: `<img class=\"\\n  tw-a tw-b\\n  hover:tw-c\\n  focus:tw-d\\n\" />`,\n            vue: `<template><img class=\"tw-a tw-b hover:tw-c focus:tw-d\" /></template>`,\n            vueOutput: `<template><img class=\"\\n  tw-a tw-b\\n  hover:tw-c\\n  focus:tw-d\\n\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.config.prefix.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.prefix.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should ignore prefixed variants in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw:a tw:b tw:hover:c tw:focus:d\" />`,\n            angularOutput: `<img class=\"\\n  tw:a tw:b\\n  tw:hover:c\\n  tw:focus:d\\n\" />`,\n            html: `<img class=\"tw:a tw:b tw:hover:c tw:focus:d\" />`,\n            htmlOutput: `<img class=\"\\n  tw:a tw:b\\n  tw:hover:c\\n  tw:focus:d\\n\" />`,\n            jsx: `() => <img class=\"tw:a tw:b tw:hover:c tw:focus:d\" />`,\n            jsxOutput: `() => <img class=\"\\n  tw:a tw:b\\n  tw:hover:c\\n  tw:focus:d\\n\" />`,\n            svelte: `<img class=\"tw:a tw:b tw:hover:c tw:focus:d\" />`,\n            svelteOutput: `<img class=\"\\n  tw:a tw:b\\n  tw:hover:c\\n  tw:focus:d\\n\" />`,\n            vue: `<template><img class=\"tw:a tw:b tw:hover:c tw:focus:d\" /></template>`,\n            vueOutput: `<template><img class=\"\\n  tw:a tw:b\\n  tw:hover:c\\n  tw:focus:d\\n\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not group arbitrary styles differently\", () => {\n    lint(\n      enforceConsistentLineWrapping,\n      {\n        valid: [\n          {\n            jsx: `() => <div class=\"md:w-full md:[height:_100px]\" />`\n          }\n        ]\n      }\n    );\n  });\n\n  describe(\"prettier compatibility\", () => {\n    const iterations = [\n      jsx`\n        () => (\n          <img class=\"font-bold text-blue\" />\n        );\n      `,\n      jsx`\n        () => (\n          <img class=\"\n            font-bold text-blue\n          \" />\n        );\n      `,\n      jsx`\n        () => (\n          <img\n            class=\"\n            font-bold text-blue\n          \"\n          />\n        );\n      `,\n      jsx`\n        () => (\n          <img\n            class=\"font-bold text-blue\"\n          />\n        );\n      `,\n      jsx`\n        () => (\n          <img class=\"font-bold text-blue\" />\n        );\n      `\n    ];\n\n    const cases = [\n      { input: iterations[0], name: \"eslint line wrapping\", output: iterations[1] },\n      { input: iterations[1], name: \"prettier class attribute newline\", output: iterations[2] },\n      { input: iterations[2], name: \"eslint line collapsing\", output: iterations[3] },\n      { input: iterations[3], name: \"prettier class attribute collapsing\", output: iterations[4] }\n    ];\n\n    it.each(cases)(\"should conflict with prettier iteration $name\", async currentCase => {\n      const index = cases.indexOf(currentCase);\n\n      const output = index % 2 === 0\n        ? await eslint(\n          currentCase.input,\n          [{\n            languageOptions: {\n              parser: eslintParserHTML\n            },\n            plugins: {\n              \"better-tailwindcss\": eslintPluginBetterTailwindcss\n            },\n            rules: {\n              \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n                printWidth: 32,\n                strictness: \"strict\"\n              }]\n            }\n          }]\n        )\n        : await prettier(\n          currentCase.input,\n          {\n            parser: \"babel\",\n            printWidth: 32\n          }\n        );\n\n      expect(output.trim()).toBe(currentCase.output);\n    });\n\n    it(`should not conflict with prettier when \"strictness\" is set to \"loose\"`, async () => {\n      const input = iterations[0];\n\n      const eslintOutput = await eslint(\n        input,\n        [{\n          languageOptions: {\n            parser: eslintParserHTML\n          },\n          plugins: {\n            \"better-tailwindcss\": eslintPluginBetterTailwindcss\n          },\n          rules: {\n            \"better-tailwindcss/enforce-consistent-line-wrapping\": [\"warn\", {\n              printWidth: 32,\n              strictness: \"loose\"\n            }]\n          }\n        }]\n      );\n\n      const prettierOutput = await prettier(\n        eslintOutput,\n        {\n          parser: \"babel\",\n          printWidth: 32\n        }\n      );\n\n      expect(eslintOutput.trim()).toBe(input.trim());\n      expect(eslintOutput.trim()).toBe(prettierOutput.trim());\n    });\n  });\n});\n"
  },
  {
    "path": "src/rules/enforce-consistent-line-wrapping.ts",
    "content": "import {\n  boolean,\n  description,\n  literal,\n  minValue,\n  number,\n  optional,\n  pipe,\n  strictObject,\n  union\n} from \"valibot\";\n\nimport { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { escapeNestedQuotes } from \"better-tailwindcss:utils/quotes.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { display, splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { DissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport type { BracesMeta, Literal, QuoteMeta, WhitespaceMeta } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\ninterface Meta extends QuoteMeta, BracesMeta, WhitespaceMeta {\n  indentation?: string;\n}\n\n\nexport const enforceConsistentLineWrapping = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce consistent line wrapping for tailwind classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-consistent-line-wrapping.md\",\n  name: \"enforce-consistent-line-wrapping\",\n  recommended: true,\n\n  messages: {\n    missing: \"Incorrect line wrapping. Expected\\n\\n{{ notReadable }}\\n\\nto be\\n\\n{{ readable }}\",\n    unnecessary: \"Unnecessary line wrapping. Expected\\n\\n{{ notReadable }}\\n\\nto be\\n\\n{{ readable }}\"\n  },\n\n  schema: strictObject({\n    classesPerLine: optional(\n      pipe(\n        number(),\n        minValue(0),\n        description(\"The maximum amount of classes per line.\")\n      ),\n      0\n    ),\n    group: optional(\n      pipe(\n        union([\n          literal(\"newLine\"),\n          literal(\"emptyLine\"),\n          literal(\"never\")\n        ]),\n        description(\"Defines how different groups of classes should be separated.\")\n      ),\n      \"newLine\"\n    ),\n    indent: optional(\n      pipe(\n        union([\n          literal(\"tab\"),\n          pipe(number(), minValue(0))\n        ]),\n        description(\"Determines how the code should be indented.\")\n      ),\n      2\n    ),\n    lineBreakStyle: optional(\n      pipe(\n        union([literal(\"unix\"),\n          literal(\"windows\")]),\n        description(\"The line break style.\")\n      ),\n\n      \"unix\"\n    ),\n    preferSingleLine: optional(\n      pipe(\n        boolean(),\n        description(\"Prefer a single line for different variants.\")\n      ),\n\n      false\n    ),\n    printWidth: optional(\n      pipe(\n        number(),\n        minValue(0),\n        description(\"The maximum line length before it gets wrapped.\")\n      ),\n      80\n    ),\n    strictness: optional(\n      pipe(\n        union([\n          literal(\"strict\"),\n          literal(\"loose\")\n        ]),\n        description(\"Enable this option if prettier is used in your project.\")\n      ),\n      \"strict\"\n    ),\n    tabWidth: optional(\n      pipe(\n        number(),\n        minValue(1),\n        description(\"The visual width of a tab character when evaluating printWidth.\")\n      ),\n      1\n    )\n  }),\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\n\nfunction lintLiterals(ctx: Context<typeof enforceConsistentLineWrapping>, literals: Literal[]) {\n  const { classesPerLine, group: groupSeparator, messageStyle, preferSingleLine, printWidth, strictness } = ctx.options;\n\n  for(const literal of literals){\n\n    if(!literal.supportsMultiline){\n      continue;\n    }\n\n    const lineStartPosition = literal.indentation + getIndentation(ctx);\n    const literalStartPosition = literal.loc.start.column;\n    const prettierStartPosition = lineStartPosition + (literal.attribute ? literal.attribute.length + 1 : 0);\n\n    const multilineClasses = new Lines(ctx, lineStartPosition);\n    const singlelineClasses = new Lines(ctx, lineStartPosition);\n\n    const classes = splitClasses(literal.content);\n\n    const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);\n\n    const invalidLineBreaks = isLineBreakStyleLikelyMisconfigured(ctx, literal.raw);\n    const invalidIndentations = isIndentationLikelyMisconfigured(ctx, literal.raw);\n\n    if(invalidLineBreaks){\n      warnings.push({\n        option: \"lineBreakStyle\",\n        title: \"Inconsistent line endings detected\",\n        url: `${ctx.docs}#linebreakstyle`\n      });\n    }\n\n    if(invalidIndentations){\n      warnings.push({\n        option: \"indent\",\n        title: \"Inconsistent indentation detected\",\n        url: `${ctx.docs}#indent`\n      });\n    }\n\n    const groupedClasses = groupClasses(classes, dissectedClasses);\n\n    if(literal.openingQuote){\n      if(literal.multilineQuotes?.includes(\"`\")){\n        multilineClasses.line.addMeta({ openingQuote: \"`\" });\n      } else {\n        multilineClasses.line.addMeta({ openingQuote: literal.openingQuote });\n      }\n    }\n\n    if(literal.openingQuote && literal.closingQuote){\n      singlelineClasses.line.addMeta({ closingQuote: literal.closingQuote, openingQuote: literal.openingQuote });\n    }\n\n    leadingTemplateLiteralNewLine: if(literal.isInterpolated && literal.closingBraces){\n\n      multilineClasses.line.addMeta({ closingBraces: literal.closingBraces });\n\n      // skip newline for sticky classes\n      if(literal.leadingWhitespace === \"\" && groupedClasses){\n        break leadingTemplateLiteralNewLine;\n      }\n\n      // skip if no classes are present\n      if(!groupedClasses){\n        break leadingTemplateLiteralNewLine;\n      }\n\n      if(groupSeparator === \"emptyLine\"){\n        multilineClasses.addLine();\n      }\n\n      if(\n        groupSeparator === \"emptyLine\" ||\n        groupSeparator === \"newLine\" ||\n        groupSeparator === \"never\"\n      ){\n        multilineClasses.addLine();\n        multilineClasses.line.indent();\n      }\n\n    }\n\n    if(groupedClasses){\n\n      for(let g = 0; g < groupedClasses.length; g++){\n\n        const group = groupedClasses.at(g)!;\n\n        const isFirstGroup = g === 0;\n\n        if(group.classCount === 0){\n          continue;\n        }\n\n        if(isFirstGroup && (\n          literal.isInterpolated && !literal.closingBraces ||\n          !literal.isInterpolated\n        )){\n          multilineClasses.addLine();\n          multilineClasses.line.indent();\n        }\n\n        if(!isFirstGroup){\n\n          if(groupSeparator === \"emptyLine\"){\n            multilineClasses.addLine();\n          }\n\n          if(\n            groupSeparator === \"emptyLine\" || groupSeparator === \"newLine\"){\n            multilineClasses.addLine();\n            multilineClasses.line.indent();\n          }\n\n        }\n\n        for(let i = 0; i < group.classCount; i++){\n\n          const isFirstClass = i === 0;\n          const isLastClass = i === group.classCount - 1;\n\n          const className = group.at(i)!;\n\n          const simulatedLine = multilineClasses.line\n            .clone()\n            .addClass(className);\n\n          // wrap after the first sticky class\n          if(\n            isFirstClass &&\n            literal.leadingWhitespace === \"\" &&\n            literal.isInterpolated &&\n            literal.closingBraces\n          ){\n\n            multilineClasses.line.addClass(className);\n\n            // don't add a new line if the first class is also the last\n            if(isLastClass){\n              break;\n            }\n\n            if(groupSeparator === \"emptyLine\"){\n              multilineClasses.addLine();\n            }\n\n            if(groupSeparator === \"emptyLine\" || groupSeparator === \"newLine\"){\n              multilineClasses.addLine();\n              multilineClasses.line.indent();\n            }\n\n            continue;\n          }\n\n          // wrap before the last sticky class\n          if(\n            isLastClass &&\n            literal.trailingWhitespace === \"\" &&\n            literal.isInterpolated &&\n            literal.openingBraces\n          ){\n\n            // skip wrapping for the first class of a group\n            if(isFirstClass){\n              multilineClasses.line.addClass(className);\n              continue;\n            }\n\n            if(groupSeparator === \"emptyLine\"){\n              multilineClasses.addLine();\n            }\n\n            if(groupSeparator === \"emptyLine\" || groupSeparator === \"newLine\"){\n              multilineClasses.addLine();\n              multilineClasses.line.indent();\n            }\n\n            multilineClasses.line.addClass(className);\n\n            continue;\n          }\n\n          // wrap if the length exceeds the limits\n          if(\n            simulatedLine.length > printWidth && printWidth !== 0 ||\n            multilineClasses.line.classCount >= classesPerLine && classesPerLine !== 0\n          ){\n\n            // but only if it is not the first class of a group or classes are not grouped\n            if(!isFirstClass || groupSeparator === \"never\"){\n              multilineClasses.addLine();\n              multilineClasses.line.indent();\n            }\n          }\n\n          multilineClasses.line.addClass(className);\n          singlelineClasses.line.addClass(className);\n\n        }\n      }\n    }\n\n    trailingTemplateLiteralNewLine: if(literal.isInterpolated && literal.openingBraces){\n\n      // skip newline for sticky classes\n      if(literal.trailingWhitespace === \"\" && groupedClasses){\n\n        multilineClasses.line.addMeta({\n          openingBraces: literal.openingBraces\n        });\n\n        break trailingTemplateLiteralNewLine;\n      }\n\n      if(groupSeparator === \"emptyLine\" && groupedClasses){\n        multilineClasses.addLine();\n      }\n\n      if(\n        groupSeparator === \"emptyLine\" ||\n        groupSeparator === \"newLine\" ||\n        groupSeparator === \"never\"\n      ){\n        multilineClasses.addLine();\n        multilineClasses.line.indent();\n      }\n\n      multilineClasses.line.addMeta({\n        openingBraces: literal.openingBraces\n      });\n\n    }\n\n    if(literal.closingQuote || literal.trailingSemicolon){\n      multilineClasses.addLine();\n      multilineClasses.line.indent(lineStartPosition - getIndentation(ctx));\n\n      if(literal.multilineQuotes?.includes(\"`\")){\n        multilineClasses.line.addMeta({ closingQuote: \"`\" });\n      } else {\n        multilineClasses.line.addMeta({ closingQuote: literal.closingQuote });\n      }\n    }\n\n    // collapse lines if there is no reason for line wrapping or if preferSingleLine is enabled\n    collapse:{\n\n      // disallow collapsing if the literal contains variants, except preferSingleLine is enabled\n      if(groupedClasses?.length !== 1 && !preferSingleLine){\n        break collapse;\n      }\n\n      // disallow collapsing for interpolated literals\n      if(literal.isInterpolated && (literal.openingBraces || literal.closingBraces)){\n        break collapse;\n      }\n\n      // disallow collapsing if the original literal was a single line (keeps original whitespace)\n      if(!literal.content.includes(getLineBreaks(ctx))){\n        break collapse;\n      }\n\n      // disallow collapsing if the single line contains more classes than the classesPerLine\n      if(singlelineClasses.line.classCount > classesPerLine && classesPerLine !== 0){\n        break collapse;\n      }\n\n      // disallow collapsing if the single line including the element and all previous characters is longer than the printWidth\n      if(literalStartPosition + singlelineClasses.line.length > printWidth && printWidth !== 0){\n        break collapse;\n      }\n\n      // add leading space for apply collapse\n      if(literal.leadingApply && !literal.leadingApply.endsWith(\" \")){\n        singlelineClasses.line.addMeta({ leadingWhitespace: \" \" });\n      }\n\n      const fixedClasses = singlelineClasses.line.toString(false);\n\n      if(literal.raw === fixedClasses){\n        continue;\n      }\n\n      ctx.report({\n        data: {\n          notReadable: display(messageStyle, literal.raw),\n          readable: display(messageStyle, fixedClasses)\n        },\n        fix: fixedClasses,\n        id: \"unnecessary\",\n        range: literal.range,\n        warnings\n      });\n\n      return;\n\n    }\n\n    // skip if class string was empty\n    if(multilineClasses.length === 2){\n      if(!literal.openingBraces && !literal.closingBraces && literal.content.trim() === \"\"){\n        continue;\n      }\n    }\n\n    // skip line wrapping if preferSingleLine is enabled and the single line does not exceed the printWidth or classesPerLine\n    if(\n      preferSingleLine &&\n      (\n        literalStartPosition + singlelineClasses.line.length <= printWidth && printWidth !== 0 ||\n        singlelineClasses.line.classCount <= classesPerLine && classesPerLine !== 0\n      ) ||\n      printWidth === 0 && classesPerLine === 0\n    ){\n      continue;\n    }\n\n    // force skip if prettier would wrap the attribute to a new line and then the single line would fit\n    if(strictness === \"loose\" &&\n      literalStartPosition + singlelineClasses.line.length > printWidth && printWidth !== 0 &&\n      prettierStartPosition + singlelineClasses.line.length <= printWidth){\n      continue;\n    }\n\n    // skip line wrapping if it is not necessary\n    skip:{\n\n      // disallow skipping if class string contains multiple groups\n      if(groupedClasses && groupedClasses.length > 1){\n        break skip;\n      }\n\n      // disallow skipping if the original literal was longer than the printWidth\n      if(\n        literalStartPosition + singlelineClasses.line.length > printWidth && printWidth !== 0 ||\n        singlelineClasses.line.classCount > classesPerLine && classesPerLine !== 0){\n        break skip;\n      }\n\n      // disallow skipping for interpolated literals\n      if(literal.isInterpolated && (literal.openingBraces || literal.closingBraces)){\n        break skip;\n      }\n\n      const openingQuoteLength = literal.openingQuote?.length ?? 0;\n      const closingBracesLength = literal.closingBraces?.length ?? 0;\n\n      const firstLineLength = multilineClasses\n        .at(1)\n        .toString()\n        .trim()\n        .length +\n        openingQuoteLength +\n        closingBracesLength;\n\n      // disallow skipping if the first line including the element and all previous characters is longer than the printWidth\n      if(literalStartPosition + firstLineLength > printWidth && printWidth !== 0){\n        break skip;\n      }\n\n      // disallow skipping if the first line contains more classes than the classesPerLine\n      if(multilineClasses.at(1).classCount > classesPerLine && classesPerLine !== 0){\n        break skip;\n      }\n\n      continue;\n\n    }\n\n    const fixedClasses = multilineClasses.toString();\n\n    if(literal.raw === fixedClasses){\n      continue;\n    }\n\n    ctx.report({\n      data: {\n        notReadable: display(messageStyle, literal.raw),\n        readable: display(messageStyle, fixedClasses)\n      },\n      fix: literal.surroundingBraces\n        ? `{${fixedClasses}}`\n        : fixedClasses,\n      id: \"missing\",\n      range: literal.range,\n      warnings\n    });\n\n  }\n\n}\n\nfunction getIndentation(ctx: Context<typeof enforceConsistentLineWrapping>): number {\n  const { indent } = ctx.options;\n  return indent === \"tab\" ? 1 : indent ?? 0;\n}\n\n\nclass Lines {\n\n  private lines: Line[] = [];\n  private currentLine: Line | undefined;\n  private indentation = 0;\n  private ctx: Context<typeof enforceConsistentLineWrapping>;\n\n  constructor(ctx: Context<typeof enforceConsistentLineWrapping>, indentation: number) {\n    this.ctx = ctx;\n    this.indentation = indentation;\n\n    this.addLine();\n  }\n\n  public at(index: number) {\n    return index >= 0\n      ? this.lines[index]\n      : this.lines[this.lines.length + index];\n  }\n\n  public get line() {\n    return this.currentLine!;\n  }\n\n  public get length() {\n    return this.lines.length;\n  }\n\n  public addLine() {\n    const line = new Line(this.ctx, this.indentation);\n    this.lines.push(line);\n    this.currentLine = line;\n    return this;\n  }\n\n  public toString() {\n    const lineBreaks = getLineBreaks(this.ctx);\n\n    return this.lines.map(\n      line => line.toString()\n    ).join(lineBreaks);\n  }\n}\n\nclass Line {\n\n  private classes: string[] = [];\n  private meta: Meta = {};\n  private ctx: Context<typeof enforceConsistentLineWrapping>;\n  private indentation = 0;\n\n  constructor(ctx: Context<typeof enforceConsistentLineWrapping>, indentation: number) {\n    this.ctx = ctx;\n    this.indentation = indentation;\n  }\n\n  public indent(start: number = this.indentation) {\n    const { indent } = this.ctx.options;\n\n    if(indent === \"tab\"){\n      this.meta.indentation = \"\\t\".repeat(start);\n    } else {\n      this.meta.indentation = \" \".repeat(start);\n    }\n\n    return this;\n  }\n\n  public get length() {\n    const line = this.toString();\n    const { tabWidth } = this.ctx.options;\n\n    if(tabWidth <= 1 || !line.includes(\"\\t\")){\n      return line.length;\n    }\n\n    let width = 0;\n\n    for(let i = 0; i < line.length; i++){\n      width += line[i] === \"\\t\"\n        ? tabWidth\n        : 1;\n    }\n\n    return width;\n  }\n\n  public get classCount() {\n    return this.classes.length;\n  }\n\n  public get printWidth() {\n    return this.toString().length;\n  }\n\n  public addMeta(meta: Meta) {\n    this.meta = { ...this.meta, ...meta };\n    return this;\n  }\n\n  public addClass(className: string) {\n    this.classes.push(className);\n    return this;\n  }\n\n  public clone() {\n    const line = new Line(this.ctx, this.indentation);\n    line.classes = [...this.classes];\n    line.meta = { ...this.meta };\n    return line;\n  }\n\n  public toString(indent: boolean = true) {\n    return this.join([\n      indent ? this.meta.indentation : \"\",\n      this.meta.openingQuote,\n      this.meta.closingBraces,\n      this.meta.leadingWhitespace ?? \"\",\n      escapeNestedQuotes(\n        this.join(this.classes),\n        this.meta.openingQuote ?? this.meta.closingQuote ?? \"`\"\n      ),\n      this.meta.trailingWhitespace ?? \"\",\n      this.meta.openingBraces,\n      this.meta.closingQuote\n    ], \"\");\n  }\n\n  private join(content: (string | undefined)[], separator: string = \" \") {\n    return content\n      .filter(content => content !== undefined)\n      .join(separator);\n  }\n}\n\nfunction groupClasses(classes: string[], dissectedClasses: DissectedClasses) {\n\n  if(classes.length === 0){\n    return;\n  }\n\n  const groups = new Groups();\n\n  for(const className of classes){\n\n    const isFirstClass = classes.indexOf(className) === 0;\n    const isFirstGroup = groups.length === 1;\n\n    const lastGroup = groups.at(-1);\n    const lastClassName = lastGroup?.at(-1);\n\n    if(lastClassName){\n      const lastDissectedClass = dissectedClasses[lastClassName];\n      const currentDissectedClass = dissectedClasses[className];\n\n      // parse variants manually for custom component classes\n      const lastVariant = lastDissectedClass.variants?.join() ?? lastClassName.match(/^(.*):/)?.[1] ?? \"\";\n      const variant = currentDissectedClass.variants?.join() ?? className.match(/^(.*):/)?.[1] ?? \"\";\n\n      if(lastVariant !== variant && !(isFirstClass && isFirstGroup)){\n        groups.addGroup();\n      }\n    }\n\n    groups.group.addClass(className);\n\n  }\n\n  return groups;\n\n}\n\nclass Groups {\n\n  public readonly groups: Group[] = [];\n  private currentGroup: Group | undefined;\n\n  constructor() {\n    this.addGroup();\n  }\n\n  public get group() {\n    return this.currentGroup!;\n  }\n\n  public at(index: number) {\n    return this.groups.at(index);\n  }\n\n  public get length() {\n    return this.groups.length;\n  }\n\n  public addGroup() {\n    const group = new Group();\n    this.currentGroup = group;\n    this.groups.push(this.currentGroup);\n    return this;\n  }\n}\n\nclass Group {\n\n  public readonly classes: string[] = [];\n\n  public get classCount() {\n    return this.classes.length;\n  }\n\n  public at(index: number) {\n    return this.classes.at(index);\n  }\n\n  public addClass(className: string) {\n    this.classes.push(className);\n    return this;\n  }\n}\n\nfunction getLineBreaks(ctx: Context<typeof enforceConsistentLineWrapping>) {\n  const { lineBreakStyle } = ctx.options;\n  return lineBreakStyle === \"unix\" ? \"\\n\" : \"\\r\\n\";\n}\n\nfunction isLineBreakStyleLikelyMisconfigured(ctx: Context<typeof enforceConsistentLineWrapping>, original: string) {\n  const { lineBreakStyle } = ctx.options;\n\n  const hasWindowsLineBreaks = original.includes(\"\\r\\n\");\n  const hasUnixLineBreaks = /(^|[^\\r])\\n/.test(original);\n\n  return (\n    hasWindowsLineBreaks && lineBreakStyle === \"unix\" ||\n    hasUnixLineBreaks && lineBreakStyle === \"windows\"\n  );\n}\n\nfunction isIndentationLikelyMisconfigured(ctx: Context<typeof enforceConsistentLineWrapping>, original: string) {\n  const { indent } = ctx.options;\n\n  const hasSpaceIndentation = /\\r?\\n +/.test(original);\n  const hasTabIndentation = /\\r?\\n\\t+/.test(original);\n\n  return (\n    hasSpaceIndentation && indent === \"tab\" ||\n    hasTabIndentation && typeof indent === \"number\"\n  );\n}\n"
  },
  {
    "path": "src/rules/enforce-consistent-variable-syntax.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentVariableSyntax } from \"better-tailwindcss:rules/enforce-consistent-variable-syntax.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\n\n\ndescribe(enforceConsistentVariableSyntax.name, () => {\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not report on the preferred syntax in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        valid: [\n          {\n            angular: `<img class=\"bg-(--brand)\" />`,\n            html: `<img class=\"bg-(--brand)\" />`,\n            jsx: `() => <img class=\"bg-(--brand)\" />`,\n            svelte: `<img class=\"bg-(--brand)\" />`,\n            vue: `<template><img class=\"bg-(--brand)\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--brand)]\" />`,\n            html: `<img class=\"bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"bg-[var(--brand)]\" /></template>`,\n\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not report on the preferred syntax in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        valid: [\n          {\n            angular: `<img class=\"bg-[--brand]\" />`,\n            html: `<img class=\"bg-[--brand]\" />`,\n            jsx: `() => <img class=\"bg-[--brand]\" />`,\n            svelte: `<img class=\"bg-[--brand]\" />`,\n            vue: `<template><img class=\"bg-[--brand]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--brand)]\" />`,\n            html: `<img class=\"bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"bg-[var(--brand)]\" /></template>`,\n\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should convert shorthands to variables\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-(--brand)\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand)]\" />`,\n            html: `<img class=\"bg-(--brand)\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"bg-(--brand)\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"bg-(--brand)\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"bg-(--brand)\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[--brand]\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand)]\" />`,\n            html: `<img class=\"bg-[--brand]\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"bg-[--brand]\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"bg-[--brand]\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"bg-[--brand]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should convert variables to parenthesized shorthands in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[var(--brand)]\" />`,\n            angularOutput: `<img class=\"bg-(--brand)\" />`,\n            html: `<img class=\"bg-[var(--brand)]\" />`,\n            htmlOutput: `<img class=\"bg-(--brand)\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand)]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--brand)\" />`,\n            svelte: `<img class=\"bg-[var(--brand)]\" />`,\n            svelteOutput: `<img class=\"bg-(--brand)\" />`,\n            vue: `<template><img class=\"bg-[var(--brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--brand)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should convert variables to arbitrary shorthands in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[var(--brand)]\" />`,\n            angularOutput: `<img class=\"bg-[--brand]\" />`,\n            html: `<img class=\"bg-[var(--brand)]\" />`,\n            htmlOutput: `<img class=\"bg-[--brand]\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand)]\" />`,\n            jsxOutput: `() => <img class=\"bg-[--brand]\" />`,\n            svelte: `<img class=\"bg-[var(--brand)]\" />`,\n            svelteOutput: `<img class=\"bg-[--brand]\" />`,\n            vue: `<template><img class=\"bg-[var(--brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--brand]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work when surrounded by underlines in arbitrary syntax in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[__var(--brand)__]\" />`,\n            angularOutput: `<img class=\"bg-(--brand)\" />`,\n            html: `<img class=\"bg-[__var(--brand)__]\" />`,\n            htmlOutput: `<img class=\"bg-(--brand)\" />`,\n            jsx: `() => <img class=\"bg-[__var(--brand)__]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--brand)\" />`,\n            svelte: `<img class=\"bg-[__var(--brand)__]\" />`,\n            svelteOutput: `<img class=\"bg-(--brand)\" />`,\n            vue: `<template><img class=\"bg-[__var(--brand)__]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--brand)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work when surrounded by underlines in arbitrary syntax in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[__var(--brand)__]\" />`,\n            angularOutput: `<img class=\"bg-[--brand]\" />`,\n            html: `<img class=\"bg-[__var(--brand)__]\" />`,\n            htmlOutput: `<img class=\"bg-[--brand]\" />`,\n            jsx: `() => <img class=\"bg-[__var(--brand)__]\" />`,\n            jsxOutput: `() => <img class=\"bg-[--brand]\" />`,\n            svelte: `<img class=\"bg-[__var(--brand)__]\" />`,\n            svelteOutput: `<img class=\"bg-[--brand]\" />`,\n            vue: `<template><img class=\"bg-[__var(--brand)__]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--brand]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with variants in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"hover:bg-(--brand)\" />`,\n            angularOutput: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            html: `<img class=\"hover:bg-(--brand)\" />`,\n            htmlOutput: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"hover:bg-(--brand)\" />`,\n            jsxOutput: `() => <img class=\"hover:bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"hover:bg-(--brand)\" />`,\n            svelteOutput: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"hover:bg-(--brand)\" /></template>`,\n            vueOutput: `<template><img class=\"hover:bg-[var(--brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            angularOutput: `<img class=\"hover:bg-(--brand)\" />`,\n            html: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            htmlOutput: `<img class=\"hover:bg-(--brand)\" />`,\n            jsx: `() => <img class=\"hover:bg-[var(--brand)]\" />`,\n            jsxOutput: `() => <img class=\"hover:bg-(--brand)\" />`,\n            svelte: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            svelteOutput: `<img class=\"hover:bg-(--brand)\" />`,\n            vue: `<template><img class=\"hover:bg-[var(--brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"hover:bg-(--brand)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with variants in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"hover:bg-[--brand]\" />`,\n            angularOutput: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            html: `<img class=\"hover:bg-[--brand]\" />`,\n            htmlOutput: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"hover:bg-[--brand]\" />`,\n            jsxOutput: `() => <img class=\"hover:bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"hover:bg-[--brand]\" />`,\n            svelteOutput: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"hover:bg-[--brand]\" /></template>`,\n            vueOutput: `<template><img class=\"hover:bg-[var(--brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            angularOutput: `<img class=\"hover:bg-[--brand]\" />`,\n            html: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            htmlOutput: `<img class=\"hover:bg-[--brand]\" />`,\n            jsx: `() => <img class=\"hover:bg-[var(--brand)]\" />`,\n            jsxOutput: `() => <img class=\"hover:bg-[--brand]\" />`,\n            svelte: `<img class=\"hover:bg-[var(--brand)]\" />`,\n            svelteOutput: `<img class=\"hover:bg-[--brand]\" />`,\n            vue: `<template><img class=\"hover:bg-[var(--brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"hover:bg-[--brand]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with other classes in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"text-red-500 bg-(--brand)\" />`,\n            angularOutput: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            html: `<img class=\"text-red-500 bg-(--brand)\" />`,\n            htmlOutput: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"text-red-500 bg-(--brand)\" />`,\n            jsxOutput: `() => <img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"text-red-500 bg-(--brand)\" />`,\n            svelteOutput: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"text-red-500 bg-(--brand)\" /></template>`,\n            vueOutput: `<template><img class=\"text-red-500 bg-[var(--brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            angularOutput: `<img class=\"text-red-500 bg-(--brand)\" />`,\n            html: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            htmlOutput: `<img class=\"text-red-500 bg-(--brand)\" />`,\n            jsx: `() => <img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            jsxOutput: `() => <img class=\"text-red-500 bg-(--brand)\" />`,\n            svelte: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            svelteOutput: `<img class=\"text-red-500 bg-(--brand)\" />`,\n            vue: `<template><img class=\"text-red-500 bg-[var(--brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"text-red-500 bg-(--brand)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with other classes <= tailwind 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"text-red-500 bg-[--brand]\" />`,\n            angularOutput: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            html: `<img class=\"text-red-500 bg-[--brand]\" />`,\n            htmlOutput: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            jsx: `() => <img class=\"text-red-500 bg-[--brand]\" />`,\n            jsxOutput: `() => <img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            svelte: `<img class=\"text-red-500 bg-[--brand]\" />`,\n            svelteOutput: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            vue: `<template><img class=\"text-red-500 bg-[--brand]\" /></template>`,\n            vueOutput: `<template><img class=\"text-red-500 bg-[var(--brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            angularOutput: `<img class=\"text-red-500 bg-[--brand]\" />`,\n            html: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            htmlOutput: `<img class=\"text-red-500 bg-[--brand]\" />`,\n            jsx: `() => <img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            jsxOutput: `() => <img class=\"text-red-500 bg-[--brand]\" />`,\n            svelte: `<img class=\"text-red-500 bg-[var(--brand)]\" />`,\n            svelteOutput: `<img class=\"text-red-500 bg-[--brand]\" />`,\n            vue: `<template><img class=\"text-red-500 bg-[var(--brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"text-red-500 bg-[--brand]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with the important modifier in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-(--brand)!\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand)]!\" />`,\n            html: `<img class=\"bg-(--brand)!\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand)]!\" />`,\n            jsx: `() => <img class=\"bg-(--brand)!\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand)]!\" />`,\n            svelte: `<img class=\"bg-(--brand)!\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand)]!\" />`,\n            vue: `<template><img class=\"bg-(--brand)!\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand)]!\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--brand)]!\" />`,\n            angularOutput: `<img class=\"bg-(--brand)!\" />`,\n            html: `<img class=\"bg-[var(--brand)]!\" />`,\n            htmlOutput: `<img class=\"bg-(--brand)!\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand)]!\" />`,\n            jsxOutput: `() => <img class=\"bg-(--brand)!\" />`,\n            svelte: `<img class=\"bg-[var(--brand)]!\" />`,\n            svelteOutput: `<img class=\"bg-(--brand)!\" />`,\n            vue: `<template><img class=\"bg-[var(--brand)]!\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--brand)!\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with the important modifier in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[--brand]!\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand)]!\" />`,\n            html: `<img class=\"bg-[--brand]!\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand)]!\" />`,\n            jsx: `() => <img class=\"bg-[--brand]!\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand)]!\" />`,\n            svelte: `<img class=\"bg-[--brand]!\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand)]!\" />`,\n            vue: `<template><img class=\"bg-[--brand]!\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand)]!\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--brand)]!\" />`,\n            angularOutput: `<img class=\"bg-[--brand]!\" />`,\n            html: `<img class=\"bg-[var(--brand)]!\" />`,\n            htmlOutput: `<img class=\"bg-[--brand]!\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand)]!\" />`,\n            jsxOutput: `() => <img class=\"bg-[--brand]!\" />`,\n            svelte: `<img class=\"bg-[var(--brand)]!\" />`,\n            svelteOutput: `<img class=\"bg-[--brand]!\" />`,\n            vue: `<template><img class=\"bg-[var(--brand)]!\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--brand]!\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should preserve fallback values in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            angularOutput: `<img class=\"bg-(--brand,_#000)\" />`,\n            html: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            htmlOutput: `<img class=\"bg-(--brand,_#000)\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand,_#000)]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--brand,_#000)\" />`,\n            svelte: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            svelteOutput: `<img class=\"bg-(--brand,_#000)\" />`,\n            vue: `<template><img class=\"bg-[var(--brand,_#000)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--brand,_#000)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"bg-(--brand,_#000)\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            html: `<img class=\"bg-(--brand,_#000)\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            jsx: `() => <img class=\"bg-(--brand,_#000)\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand,_#000)]\" />`,\n            svelte: `<img class=\"bg-(--brand,_#000)\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            vue: `<template><img class=\"bg-(--brand,_#000)\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand,_#000)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should preserve fallback values in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            angularOutput: `<img class=\"bg-[--brand,_#000]\" />`,\n            html: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            htmlOutput: `<img class=\"bg-[--brand,_#000]\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand,_#000)]\" />`,\n            jsxOutput: `() => <img class=\"bg-[--brand,_#000]\" />`,\n            svelte: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            svelteOutput: `<img class=\"bg-[--brand,_#000]\" />`,\n            vue: `<template><img class=\"bg-[var(--brand,_#000)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--brand,_#000]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"bg-[--brand,_#000]\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            html: `<img class=\"bg-[--brand,_#000]\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            jsx: `() => <img class=\"bg-[--brand,_#000]\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand,_#000)]\" />`,\n            svelte: `<img class=\"bg-[--brand,_#000]\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand,_#000)]\" />`,\n            vue: `<template><img class=\"bg-[--brand,_#000]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand,_#000)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should preserve css functions in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            angularOutput: `<img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            html: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            htmlOutput: `<img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            jsx: `() => <img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            jsxOutput: `() => <img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            svelte: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            svelteOutput: `<img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            vue: `<template><img class=\"height-[var(--header,calc(100%_-_1rem))]\" /></template>`,\n            vueOutput: `<template><img class=\"height-(--header,calc(100%_-_1rem))\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            angularOutput: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            html: `<img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            htmlOutput: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            jsx: `() => <img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            jsxOutput: `() => <img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            svelte: `<img class=\"height-(--header,calc(100%_-_1rem))\" />`,\n            svelteOutput: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            vue: `<template><img class=\"height-(--header,calc(100%_-_1rem))\" /></template>`,\n            vueOutput: `<template><img class=\"height-[var(--header,calc(100%_-_1rem))]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should preserve css functions in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            angularOutput: `<img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            html: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            htmlOutput: `<img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            jsx: `() => <img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            jsxOutput: `() => <img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            svelte: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            svelteOutput: `<img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            vue: `<template><img class=\"height-[var(--header,calc(100%_-_1rem))]\" /></template>`,\n            vueOutput: `<template><img class=\"height-[--header,calc(100%_-_1rem)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            angularOutput: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            html: `<img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            htmlOutput: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            jsx: `() => <img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            jsxOutput: `() => <img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            svelte: `<img class=\"height-[--header,calc(100%_-_1rem)]\" />`,\n            svelteOutput: `<img class=\"height-[var(--header,calc(100%_-_1rem))]\" />`,\n            vue: `<template><img class=\"height-[--header,calc(100%_-_1rem)]\" /></template>`,\n            vueOutput: `<template><img class=\"height-[var(--header,calc(100%_-_1rem))]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with nested variables in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            angularOutput: `<img class=\"bg-(--brand,var(--secondary))\" />`,\n            html: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            htmlOutput: `<img class=\"bg-(--brand,var(--secondary))\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--brand,var(--secondary))\" />`,\n            svelte: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            svelteOutput: `<img class=\"bg-(--brand,var(--secondary))\" />`,\n            vue: `<template><img class=\"bg-[var(--brand,var(--secondary))]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--brand,var(--secondary))\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"bg-(--brand,var(--secondary))\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            html: `<img class=\"bg-(--brand,var(--secondary))\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            jsx: `() => <img class=\"bg-(--brand,var(--secondary))\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            svelte: `<img class=\"bg-(--brand,var(--secondary))\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            vue: `<template><img class=\"bg-(--brand,var(--secondary))\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand,var(--secondary))]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with nested variables in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            angularOutput: `<img class=\"bg-[--brand,var(--secondary)]\" />`,\n            html: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            htmlOutput: `<img class=\"bg-[--brand,var(--secondary)]\" />`,\n            jsx: `() => <img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            jsxOutput: `() => <img class=\"bg-[--brand,var(--secondary)]\" />`,\n            svelte: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            svelteOutput: `<img class=\"bg-[--brand,var(--secondary)]\" />`,\n            vue: `<template><img class=\"bg-[var(--brand,var(--secondary))]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--brand,var(--secondary)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"bg-[--brand,var(--secondary)]\" />`,\n            angularOutput: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            html: `<img class=\"bg-[--brand,var(--secondary)]\" />`,\n            htmlOutput: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            jsx: `() => <img class=\"bg-[--brand,var(--secondary)]\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            svelte: `<img class=\"bg-[--brand,var(--secondary)]\" />`,\n            svelteOutput: `<img class=\"bg-[var(--brand,var(--secondary))]\" />`,\n            vue: `<template><img class=\"bg-[--brand,var(--secondary)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--brand,var(--secondary))]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should preserve the case sensitivity of the variable name in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-(--Brand)\" />`,\n            angularOutput: `<img class=\"bg-[var(--Brand)]\" />`,\n            html: `<img class=\"bg-(--Brand)\" />`,\n            htmlOutput: `<img class=\"bg-[var(--Brand)]\" />`,\n            jsx: `() => <img class=\"bg-(--Brand)\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--Brand)]\" />`,\n            svelte: `<img class=\"bg-(--Brand)\" />`,\n            svelteOutput: `<img class=\"bg-[var(--Brand)]\" />`,\n            vue: `<template><img class=\"bg-(--Brand)\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--Brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--Brand)]\" />`,\n            angularOutput: `<img class=\"bg-(--Brand)\" />`,\n            html: `<img class=\"bg-[var(--Brand)]\" />`,\n            htmlOutput: `<img class=\"bg-(--Brand)\" />`,\n            jsx: `() => <img class=\"bg-[var(--Brand)]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--Brand)\" />`,\n            svelte: `<img class=\"bg-[var(--Brand)]\" />`,\n            svelteOutput: `<img class=\"bg-(--Brand)\" />`,\n            vue: `<template><img class=\"bg-[var(--Brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--Brand)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should preserve the case sensitivity of the variable name in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[--Brand]\" />`,\n            angularOutput: `<img class=\"bg-[var(--Brand)]\" />`,\n            html: `<img class=\"bg-[--Brand]\" />`,\n            htmlOutput: `<img class=\"bg-[var(--Brand)]\" />`,\n            jsx: `() => <img class=\"bg-[--Brand]\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--Brand)]\" />`,\n            svelte: `<img class=\"bg-[--Brand]\" />`,\n            svelteOutput: `<img class=\"bg-[var(--Brand)]\" />`,\n            vue: `<template><img class=\"bg-[--Brand]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--Brand)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--Brand)]\" />`,\n            angularOutput: `<img class=\"bg-[--Brand]\" />`,\n            html: `<img class=\"bg-[var(--Brand)]\" />`,\n            htmlOutput: `<img class=\"bg-[--Brand]\" />`,\n            jsx: `() => <img class=\"bg-[var(--Brand)]\" />`,\n            jsxOutput: `() => <img class=\"bg-[--Brand]\" />`,\n            svelte: `<img class=\"bg-[var(--Brand)]\" />`,\n            svelteOutput: `<img class=\"bg-[--Brand]\" />`,\n            vue: `<template><img class=\"bg-[var(--Brand)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--Brand]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should preserve allow special characters in variable names in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-(--special_variable_😏)\" />`,\n            angularOutput: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            html: `<img class=\"bg-(--special_variable_😏)\" />`,\n            htmlOutput: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            jsx: `() => <img class=\"bg-(--special_variable_😏)\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--special_variable_😏)]\" />`,\n            svelte: `<img class=\"bg-(--special_variable_😏)\" />`,\n            svelteOutput: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            vue: `<template><img class=\"bg-(--special_variable_😏)\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--special_variable_😏)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            angularOutput: `<img class=\"bg-(--special_variable_😏)\" />`,\n            html: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            htmlOutput: `<img class=\"bg-(--special_variable_😏)\" />`,\n            jsx: `() => <img class=\"bg-[var(--special_variable_😏)]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--special_variable_😏)\" />`,\n            svelte: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            svelteOutput: `<img class=\"bg-(--special_variable_😏)\" />`,\n            vue: `<template><img class=\"bg-[var(--special_variable_😏)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--special_variable_😏)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should preserve allow special characters in variable names in tailwind <= 3\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[--special_variable_😏]\" />`,\n            angularOutput: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            html: `<img class=\"bg-[--special_variable_😏]\" />`,\n            htmlOutput: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            jsx: `() => <img class=\"bg-[--special_variable_😏]\" />`,\n            jsxOutput: `() => <img class=\"bg-[var(--special_variable_😏)]\" />`,\n            svelte: `<img class=\"bg-[--special_variable_😏]\" />`,\n            svelteOutput: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            vue: `<template><img class=\"bg-[--special_variable_😏]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[var(--special_variable_😏)]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            angularOutput: `<img class=\"bg-[--special_variable_😏]\" />`,\n            html: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            htmlOutput: `<img class=\"bg-[--special_variable_😏]\" />`,\n            jsx: `() => <img class=\"bg-[var(--special_variable_😏)]\" />`,\n            jsxOutput: `() => <img class=\"bg-[--special_variable_😏]\" />`,\n            svelte: `<img class=\"bg-[var(--special_variable_😏)]\" />`,\n            svelteOutput: `<img class=\"bg-[--special_variable_😏]\" />`,\n            vue: `<template><img class=\"bg-[var(--special_variable_😏)]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-[--special_variable_😏]\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with multiline classes in tailwind >= 4\", () => {\n\n    const multilineShorthand = dedent`\n      bg-(--primary)\n      hover:bg-(--secondary)\n    `;\n    const multilineArbitrary = dedent`\n      bg-[var(--primary)]\n      hover:bg-[var(--secondary)]\n    `;\n\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${multilineShorthand}\" />`,\n            angularOutput: `<img class=\"${multilineArbitrary}\" />`,\n            html: `<img class=\"${multilineShorthand}\" />`,\n            htmlOutput: `<img class=\"${multilineArbitrary}\" />`,\n            jsx: `() => <img class={\\`${multilineShorthand}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineArbitrary}\\`} />`,\n            svelte: `<img class=\"${multilineShorthand}\" />`,\n            svelteOutput: `<img class=\"${multilineArbitrary}\" />`,\n            vue: `<template><img class=\"${multilineShorthand}\" /></template>`,\n            vueOutput: `<template><img class=\"${multilineArbitrary}\" /></template>`,\n\n            errors: 2,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"${multilineArbitrary}\" />`,\n            angularOutput: `<img class=\"${multilineShorthand}\" />`,\n            html: `<img class=\"${multilineArbitrary}\" />`,\n            htmlOutput: `<img class=\"${multilineShorthand}\" />`,\n            jsx: `() => <img class={\\`${multilineArbitrary}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineShorthand}\\`} />`,\n            svelte: `<img class=\"${multilineArbitrary}\" />`,\n            svelteOutput: `<img class=\"${multilineShorthand}\" />`,\n            vue: `<template><img class=\"${multilineArbitrary}\" /></template>`,\n            vueOutput: `<template><img class=\"${multilineShorthand}\" /></template>`,\n\n            errors: 2,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with multiline classes in tailwind <= 3\", () => {\n\n    const multilineShorthand = dedent`\n      bg-[--primary]\n      hover:bg-[--secondary]\n    `;\n    const multilineArbitrary = dedent`\n      bg-[var(--primary)]\n      hover:bg-[var(--secondary)]\n    `;\n\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${multilineShorthand}\" />`,\n            angularOutput: `<img class=\"${multilineArbitrary}\" />`,\n            html: `<img class=\"${multilineShorthand}\" />`,\n            htmlOutput: `<img class=\"${multilineArbitrary}\" />`,\n            jsx: `() => <img class={\\`${multilineShorthand}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineArbitrary}\\`} />`,\n            svelte: `<img class=\"${multilineShorthand}\" />`,\n            svelteOutput: `<img class=\"${multilineArbitrary}\" />`,\n            vue: `<template><img class=\"${multilineShorthand}\" /></template>`,\n            vueOutput: `<template><img class=\"${multilineArbitrary}\" /></template>`,\n\n            errors: 2,\n            options: [{ syntax: \"variable\" }]\n          },\n          {\n            angular: `<img class=\"${multilineArbitrary}\" />`,\n            angularOutput: `<img class=\"${multilineShorthand}\" />`,\n            html: `<img class=\"${multilineArbitrary}\" />`,\n            htmlOutput: `<img class=\"${multilineShorthand}\" />`,\n            jsx: `() => <img class={\\`${multilineArbitrary}\\`} />`,\n            jsxOutput: `() => <img class={\\`${multilineShorthand}\\`} />`,\n            svelte: `<img class=\"${multilineArbitrary}\" />`,\n            svelteOutput: `<img class=\"${multilineShorthand}\" />`,\n            vue: `<template><img class=\"${multilineArbitrary}\" /></template>`,\n            vueOutput: `<template><img class=\"${multilineShorthand}\" /></template>`,\n\n            errors: 2,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should convert arbitrary shorthands to parenthesized shorthands in tailwind >= 4\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-[--brand]\" />`,\n            angularOutput: `<img class=\"bg-(--brand)\" />`,\n            html: `<img class=\"bg-[--brand]\" />`,\n            htmlOutput: `<img class=\"bg-(--brand)\" />`,\n            jsx: `() => <img class=\"bg-[--brand]\" />`,\n            jsxOutput: `() => <img class=\"bg-(--brand)\" />`,\n            svelte: `<img class=\"bg-[--brand]\" />`,\n            svelteOutput: `<img class=\"bg-(--brand)\" />`,\n            vue: `<template><img class=\"bg-[--brand]\" /></template>`,\n            vueOutput: `<template><img class=\"bg-(--brand)\" /></template>`,\n\n            errors: 1,\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not convert variable definitions\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        valid: [\n          {\n            angular: `<img class=\"[--green:#00ff00]\" />`,\n            html: `<img class=\"[--green:#00ff00]\" />`,\n            jsx: `() => <img class=\"[--green:#00ff00]\" />`,\n            svelte: `<img class=\"[--green:#00ff00]\" />`,\n            vue: `<template><img class=\"[--green:#00ff00]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"hover:[--green:#00ff00]\" />`,\n            html: `<img class=\"hover:[--green:#00ff00]\" />`,\n            jsx: `() => <img class=\"hover:[--green:#00ff00]\" />`,\n            svelte: `<img class=\"hover:[--green:#00ff00]\" />`,\n            vue: `<template><img class=\"hover:[--green:#00ff00]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"tw:[--green:#00ff00]\" />`,\n            html: `<img class=\"tw:[--green:#00ff00]\" />`,\n            jsx: `() => <img class=\"tw:[--green:#00ff00]\" />`,\n            svelte: `<img class=\"tw:[--green:#00ff00]\" />`,\n            vue: `<template><img class=\"tw:[--green:#00ff00]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not convert arbitrary variables in arbitrary classes\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        valid: [\n          {\n            angular: `<img class=\"[background-color:var(--secondary)]\" />`,\n            html: `<img class=\"[background-color:var(--secondary)]\" />`,\n            jsx: `() => <img class=\"[background-color:var(--secondary)]\" />`,\n            svelte: `<img class=\"[background-color:var(--secondary)]\" />`,\n            vue: `<template><img class=\"[background-color:var(--secondary)]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not convert if multiple variables are used in the same class\", () => {\n    lint(\n      enforceConsistentVariableSyntax,\n      {\n        valid: [\n          {\n            angular: `<img class=\"object-[var(--x)_var(--y)]\" />`,\n            html: `<img class=\"object-[var(--x)_var(--y)]\" />`,\n            jsx: `() => <img class=\"object-[var(--x)_var(--y)]\" />`,\n            svelte: `<img class=\"object-[var(--x)_var(--y)]\" />`,\n            vue: `<template><img class=\"object-[var(--x)_var(--y)]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\n    \"should not report on custom css functions\",\n    () => {\n      lint(enforceConsistentVariableSyntax, {\n        valid: [\n          {\n            angular: `<img class=\"grid-cols-[--spacing(24)_1fr]\" />`,\n            html: `<img class=\"grid-cols-[--spacing(24)_1fr]\" />`,\n            jsx: `() => <img class=\"grid-cols-[--spacing(24)_1fr]\" />`,\n            svelte: `<img class=\"grid-cols-[--spacing(24)_1fr]\" />`,\n            vue: `<template><img class=\"grid-cols-[--spacing(24)_1fr]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"grid-cols-[--fn()]\" />`,\n            html: `<img class=\"grid-cols-[--fn()]\" />`,\n            jsx: `() => <img class=\"grid-cols-[--fn()]\" />`,\n            svelte: `<img class=\"grid-cols-[--fn()]\" />`,\n            vue: `<template><img class=\"grid-cols-[--fn()]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"grid-cols-[--spacing(1,2,3)]\" />`,\n            html: `<img class=\"grid-cols-[--spacing(1,2,3)]\" />`,\n            jsx: `() => <img class=\"grid-cols-[--spacing(1,2,3)]\" />`,\n            svelte: `<img class=\"grid-cols-[--spacing(1,2,3)]\" />`,\n            vue: `<template><img class=\"grid-cols-[--spacing(1,2,3)]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          },\n          {\n            angular: `<img class=\"grid-cols-[--my-func(x)]\" />`,\n            html: `<img class=\"grid-cols-[--my-func(x)]\" />`,\n            jsx: `() => <img class=\"grid-cols-[--my-func(x)]\" />`,\n            svelte: `<img class=\"grid-cols-[--my-func(x)]\" />`,\n            vue: `<template><img class=\"grid-cols-[--my-func(x)]\" /></template>`,\n\n            options: [{ syntax: \"shorthand\" }]\n          }\n        ]\n      });\n    }\n  );\n});\n"
  },
  {
    "path": "src/rules/enforce-consistent-variable-syntax.ts",
    "content": "import { description, literal, optional, pipe, strictObject, union } from \"valibot\";\n\nimport { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { buildClass } from \"better-tailwindcss:utils/class.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { getCachedRegex } from \"better-tailwindcss:utils/regex.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const enforceConsistentVariableSyntax = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce consistent syntax for css variables.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-consistent-variable-syntax.md\",\n  name: \"enforce-consistent-variable-syntax\",\n  recommended: false,\n\n  messages: {\n    incorrect: \"Incorrect variable syntax: {{ className }}.\"\n  },\n\n  schema: strictObject({\n    syntax: optional(\n      pipe(\n        union([\n          literal(\"shorthand\"),\n          literal(\"variable\")\n        ]),\n        description(\"The syntax to enforce for css variables in tailwindcss class strings.\")\n      ),\n      \"shorthand\"\n    )\n  }),\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\n\nfunction lintLiterals(ctx: Context<typeof enforceConsistentVariableSyntax>, literals: Literal[]) {\n\n  const { syntax } = ctx.options;\n\n  for(const literal of literals){\n    const classes = splitClasses(literal.content);\n\n    const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);\n\n    lintClasses(ctx, literal, className => {\n      const dissectedClass = dissectedClasses[className];\n\n      if(!dissectedClass){\n        return;\n      }\n\n      // skip variable definitions\n      if(dissectedClass.base.includes(\":\")){\n        return;\n      }\n\n      const {\n        after: afterParentheses,\n        before: beforeParentheses,\n        characters: charactersParentheses\n      } = extractBalanced(dissectedClass.base);\n\n      const {\n        after: afterSquareBrackets,\n        before: beforeSquareBrackets,\n        characters: charactersSquareBrackets\n      } = extractBalanced(dissectedClass.base, \"[\", \"]\");\n\n      if(syntax === \"shorthand\"){\n\n        if(!charactersSquareBrackets){\n          return;\n        }\n\n        if(isBeginningOfArbitraryVariable(charactersSquareBrackets)){\n\n          const { after, characters } = extractBalanced(charactersSquareBrackets);\n\n          if(trimTailwindWhitespace(after).length > 0){\n            return;\n          }\n\n          const fixedClass = ctx.version.major >= 4\n            ? buildClass(ctx, { ...dissectedClass, base: [...beforeSquareBrackets, `(${characters})`, ...afterSquareBrackets].join(\"\") })\n            : buildClass(ctx, { ...dissectedClass, base: [...beforeSquareBrackets, `[${characters}]`, ...afterSquareBrackets].join(\"\") });\n\n          return {\n            data: { className },\n            fix: fixedClass,\n            id: \"incorrect\",\n            warnings\n          } as const;\n\n        }\n\n        if(isBeginningOfArbitraryShorthand(charactersSquareBrackets)){\n          if(ctx.version.major <= 3){\n            return;\n          }\n\n          const fixedClass = buildClass(ctx, {\n            ...dissectedClass,\n            base: [...beforeSquareBrackets, `(${charactersSquareBrackets})`, ...afterSquareBrackets].join(\"\")\n          });\n\n          return {\n            data: { className },\n            fix: fixedClass,\n            id: \"incorrect\",\n            warnings\n          } as const;\n\n        }\n      }\n\n      if(syntax === \"variable\"){\n\n        if(charactersSquareBrackets && isBeginningOfArbitraryVariable(charactersSquareBrackets)){\n          return;\n        }\n\n        if(isBeginningOfArbitraryShorthand(charactersSquareBrackets)){\n\n          const fixedClass = buildClass(ctx, {\n            ...dissectedClass,\n            base: [...beforeSquareBrackets, `[var(${charactersSquareBrackets})]`, ...afterSquareBrackets].join(\"\")\n          });\n\n          return {\n            data: { className },\n            fix: fixedClass,\n            id: \"incorrect\",\n            warnings\n          } as const;\n        }\n\n        if(isBeginningOfArbitraryShorthand(charactersParentheses)){\n\n          const fixedClass = buildClass(ctx, {\n            ...dissectedClass,\n            base: [\n              ...beforeParentheses,\n              `[var(${charactersParentheses})]`,\n              ...afterParentheses\n            ].join(\"\")\n          });\n\n          return {\n            data: { className },\n            fix: fixedClass,\n            id: \"incorrect\",\n            warnings\n          } as const;\n\n        }\n      }\n\n    });\n  }\n}\n\nfunction isBeginningOfArbitraryShorthand(base: string): boolean {\n  return getCachedRegex(/^_*--(?![\\w-]+\\()/).test(base);\n}\n\nfunction isBeginningOfArbitraryVariable(base: string): boolean {\n  return getCachedRegex(/^_*var\\(_*--/).test(base);\n}\n\nfunction extractBalanced(className: string, start = \"(\", end = \")\") {\n  const before: string[] = [];\n  const characters: string[] = [];\n  const after: string[] = [];\n\n  for(let i = 0, parenthesesCount = 0, hasStarted: boolean = false, hasEnded: boolean = false; i < className.length; i++){\n    if(className[i] === start){\n      parenthesesCount++;\n\n      if(!hasStarted){\n        hasStarted = true;\n        continue;\n      }\n    }\n\n    if(!hasStarted && !hasEnded){\n      before.push(className[i]);\n      continue;\n    }\n\n    if(className[i] === end){\n      parenthesesCount--;\n\n      if(parenthesesCount === 0){\n        hasEnded = true;\n        continue;\n      }\n    }\n\n    if(!hasEnded){\n      characters.push(className[i]);\n      continue;\n    } else {\n      after.push(className[i]);\n    }\n  }\n\n  return {\n    after: after.join(\"\"),\n    before: before.join(\"\"),\n    characters: characters.join(\"\")\n  };\n}\n\nfunction trimTailwindWhitespace(className: string): string {\n  return className.replace(/^_+|_+$/g, \"\");\n}\n"
  },
  {
    "path": "src/rules/enforce-consistent-variant-order.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceConsistentVariantOrder } from \"better-tailwindcss:rules/enforce-consistent-variant-order.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version.js\";\n\n\ndescribe.runIf(getTailwindCSSVersion().major >= 4)(enforceConsistentVariantOrder.name, () => {\n\n  it(\"should move global variants to the beginning\", () => {\n    lint(enforceConsistentVariantOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"hover:lg:text-red-500\" />`,\n          angularOutput: `<img class=\"lg:hover:text-red-500\" />`,\n          astro: `<img class=\"hover:lg:text-red-500\" />`,\n          astroOutput: `<img class=\"lg:hover:text-red-500\" />`,\n          html: `<img class=\"hover:lg:text-red-500\" />`,\n          htmlOutput: `<img class=\"lg:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:lg:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"lg:hover:text-red-500\" />`,\n          svelte: `<img class=\"hover:lg:text-red-500\" />`,\n          svelteOutput: `<img class=\"lg:hover:text-red-500\" />`,\n          vue: `<template><img class=\"hover:lg:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"lg:hover:text-red-500\" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class=\"focus:hover:lg:text-red-500\" />`,\n          angularOutput: `<img class=\"lg:focus:hover:text-red-500\" />`,\n          astro: `<img class=\"focus:hover:lg:text-red-500\" />`,\n          astroOutput: `<img class=\"lg:focus:hover:text-red-500\" />`,\n          html: `<img class=\"focus:hover:lg:text-red-500\" />`,\n          htmlOutput: `<img class=\"lg:focus:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"focus:hover:lg:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"lg:focus:hover:text-red-500\" />`,\n          svelte: `<img class=\"focus:hover:lg:text-red-500\" />`,\n          svelteOutput: `<img class=\"lg:focus:hover:text-red-500\" />`,\n          vue: `<template><img class=\"focus:hover:lg:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"lg:focus:hover:text-red-500\" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class=\"hover:lg:dark:text-red-500\" />`,\n          angularOutput: `<img class=\"dark:lg:hover:text-red-500\" />`,\n          astro: `<img class=\"hover:lg:dark:text-red-500\" />`,\n          astroOutput: `<img class=\"dark:lg:hover:text-red-500\" />`,\n          html: `<img class=\"hover:lg:dark:text-red-500\" />`,\n          htmlOutput: `<img class=\"dark:lg:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:lg:dark:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"dark:lg:hover:text-red-500\" />`,\n          svelte: `<img class=\"hover:lg:dark:text-red-500\" />`,\n          svelteOutput: `<img class=\"dark:lg:hover:text-red-500\" />`,\n          vue: `<template><img class=\"hover:lg:dark:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"dark:lg:hover:text-red-500\" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should treat media and feature-query variants as global\", () => {\n    lint(enforceConsistentVariantOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"hover:print:text-red-500\" />`,\n          angularOutput: `<img class=\"print:hover:text-red-500\" />`,\n          astro: `<img class=\"hover:print:text-red-500\" />`,\n          astroOutput: `<img class=\"print:hover:text-red-500\" />`,\n          html: `<img class=\"hover:print:text-red-500\" />`,\n          htmlOutput: `<img class=\"print:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:print:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"print:hover:text-red-500\" />`,\n          svelte: `<img class=\"hover:print:text-red-500\" />`,\n          svelteOutput: `<img class=\"print:hover:text-red-500\" />`,\n          vue: `<template><img class=\"hover:print:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"print:hover:text-red-500\" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class=\"hover:motion-reduce:text-red-500\" />`,\n          angularOutput: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          astro: `<img class=\"hover:motion-reduce:text-red-500\" />`,\n          astroOutput: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          html: `<img class=\"hover:motion-reduce:text-red-500\" />`,\n          htmlOutput: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:motion-reduce:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"motion-reduce:hover:text-red-500\" />`,\n          svelte: `<img class=\"hover:motion-reduce:text-red-500\" />`,\n          svelteOutput: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          vue: `<template><img class=\"hover:motion-reduce:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"motion-reduce:hover:text-red-500\" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class=\"hover:forced-colors:text-red-500\" />`,\n          angularOutput: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          astro: `<img class=\"hover:forced-colors:text-red-500\" />`,\n          astroOutput: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          html: `<img class=\"hover:forced-colors:text-red-500\" />`,\n          htmlOutput: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:forced-colors:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"forced-colors:hover:text-red-500\" />`,\n          svelte: `<img class=\"hover:forced-colors:text-red-500\" />`,\n          svelteOutput: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          vue: `<template><img class=\"hover:forced-colors:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"forced-colors:hover:text-red-500\" /></template>`,\n\n          errors: 1\n        }\n      ],\n      valid: [\n        {\n          angular: `<img class=\"print:hover:text-red-500\" />`,\n          astro: `<img class=\"print:hover:text-red-500\" />`,\n          html: `<img class=\"print:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"print:hover:text-red-500\" />`,\n          svelte: `<img class=\"print:hover:text-red-500\" />`,\n          vue: `<template><img class=\"print:hover:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          astro: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          html: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"motion-reduce:hover:text-red-500\" />`,\n          svelte: `<img class=\"motion-reduce:hover:text-red-500\" />`,\n          vue: `<template><img class=\"motion-reduce:hover:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          astro: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          html: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"forced-colors:hover:text-red-500\" />`,\n          svelte: `<img class=\"forced-colors:hover:text-red-500\" />`,\n          vue: `<template><img class=\"forced-colors:hover:text-red-500\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should support sorting around arbitrary variants\", () => {\n    lint(enforceConsistentVariantOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"[&.is-dragging]:lg:cursor-grabbing\" />`,\n          angularOutput: `<img class=\"lg:[&.is-dragging]:cursor-grabbing\" />`,\n          astro: `<img class=\"[&.is-dragging]:lg:cursor-grabbing\" />`,\n          astroOutput: `<img class=\"lg:[&.is-dragging]:cursor-grabbing\" />`,\n          html: `<img class=\"[&.is-dragging]:lg:cursor-grabbing\" />`,\n          htmlOutput: `<img class=\"lg:[&.is-dragging]:cursor-grabbing\" />`,\n          jsx: `() => <img class=\"[&.is-dragging]:lg:cursor-grabbing\" />`,\n          jsxOutput: `() => <img class=\"lg:[&.is-dragging]:cursor-grabbing\" />`,\n          svelte: `<img class=\"[&.is-dragging]:lg:cursor-grabbing\" />`,\n          svelteOutput: `<img class=\"lg:[&.is-dragging]:cursor-grabbing\" />`,\n          vue: `<template><img class=\"[&.is-dragging]:lg:cursor-grabbing\" /></template>`,\n          vueOutput: `<template><img class=\"lg:[&.is-dragging]:cursor-grabbing\" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should preserve modifiers when sorting variants\", () => {\n    lint(enforceConsistentVariantOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"group-hover/name:lg:text-red-500\" />`,\n          angularOutput: `<img class=\"lg:group-hover/name:text-red-500\" />`,\n          astro: `<img class=\"group-hover/name:lg:text-red-500\" />`,\n          astroOutput: `<img class=\"lg:group-hover/name:text-red-500\" />`,\n          html: `<img class=\"group-hover/name:lg:text-red-500\" />`,\n          htmlOutput: `<img class=\"lg:group-hover/name:text-red-500\" />`,\n          jsx: `() => <img class=\"group-hover/name:lg:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"lg:group-hover/name:text-red-500\" />`,\n          svelte: `<img class=\"group-hover/name:lg:text-red-500\" />`,\n          svelteOutput: `<img class=\"lg:group-hover/name:text-red-500\" />`,\n          vue: `<template><img class=\"group-hover/name:lg:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"lg:group-hover/name:text-red-500\" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should treat child selector variants as non-global\", () => {\n    lint(enforceConsistentVariantOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"*:lg:text-red-500\" />`,\n          angularOutput: `<img class=\"lg:*:text-red-500\" />`,\n          astro: `<img class=\"*:lg:text-red-500\" />`,\n          astroOutput: `<img class=\"lg:*:text-red-500\" />`,\n          html: `<img class=\"*:lg:text-red-500\" />`,\n          htmlOutput: `<img class=\"lg:*:text-red-500\" />`,\n          jsx: `() => <img class=\"*:lg:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"lg:*:text-red-500\" />`,\n          svelte: `<img class=\"*:lg:text-red-500\" />`,\n          svelteOutput: `<img class=\"lg:*:text-red-500\" />`,\n          vue: `<template><img class=\"*:lg:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"lg:*:text-red-500\" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class=\"**:lg:text-red-500\" />`,\n          angularOutput: `<img class=\"lg:**:text-red-500\" />`,\n          astro: `<img class=\"**:lg:text-red-500\" />`,\n          astroOutput: `<img class=\"lg:**:text-red-500\" />`,\n          html: `<img class=\"**:lg:text-red-500\" />`,\n          htmlOutput: `<img class=\"lg:**:text-red-500\" />`,\n          jsx: `() => <img class=\"**:lg:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"lg:**:text-red-500\" />`,\n          svelte: `<img class=\"**:lg:text-red-500\" />`,\n          svelteOutput: `<img class=\"lg:**:text-red-500\" />`,\n          vue: `<template><img class=\"**:lg:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"lg:**:text-red-500\" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class=\"*:**:lg:text-red-500\" />`,\n          angularOutput: `<img class=\"lg:*:**:text-red-500\" />`,\n          astro: `<img class=\"*:**:lg:text-red-500\" />`,\n          astroOutput: `<img class=\"lg:*:**:text-red-500\" />`,\n          html: `<img class=\"*:**:lg:text-red-500\" />`,\n          htmlOutput: `<img class=\"lg:*:**:text-red-500\" />`,\n          jsx: `() => <img class=\"*:**:lg:text-red-500\" />`,\n          jsxOutput: `() => <img class=\"lg:*:**:text-red-500\" />`,\n          svelte: `<img class=\"*:**:lg:text-red-500\" />`,\n          svelteOutput: `<img class=\"lg:*:**:text-red-500\" />`,\n          vue: `<template><img class=\"*:**:lg:text-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"lg:*:**:text-red-500\" /></template>`,\n\n          errors: 1\n        }\n      ],\n      valid: [\n        {\n          angular: `<img class=\"*:hover:text-red-500\" />`,\n          astro: `<img class=\"*:hover:text-red-500\" />`,\n          html: `<img class=\"*:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"*:hover:text-red-500\" />`,\n          svelte: `<img class=\"*:hover:text-red-500\" />`,\n          vue: `<template><img class=\"*:hover:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"hover:*:text-red-500\" />`,\n          astro: `<img class=\"hover:*:text-red-500\" />`,\n          html: `<img class=\"hover:*:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:*:text-red-500\" />`,\n          svelte: `<img class=\"hover:*:text-red-500\" />`,\n          vue: `<template><img class=\"hover:*:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"**:hover:text-red-500\" />`,\n          astro: `<img class=\"**:hover:text-red-500\" />`,\n          html: `<img class=\"**:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"**:hover:text-red-500\" />`,\n          svelte: `<img class=\"**:hover:text-red-500\" />`,\n          vue: `<template><img class=\"**:hover:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"hover:**:text-red-500\" />`,\n          astro: `<img class=\"hover:**:text-red-500\" />`,\n          html: `<img class=\"hover:**:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:**:text-red-500\" />`,\n          svelte: `<img class=\"hover:**:text-red-500\" />`,\n          vue: `<template><img class=\"hover:**:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"*:first:appearance-auto\" />`,\n          astro: `<img class=\"*:first:appearance-auto\" />`,\n          html: `<img class=\"*:first:appearance-auto\" />`,\n          jsx: `() => <img class=\"*:first:appearance-auto\" />`,\n          svelte: `<img class=\"*:first:appearance-auto\" />`,\n          vue: `<template><img class=\"*:first:appearance-auto\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should support custom breakpoints\", () => {\n    lint(enforceConsistentVariantOrder, {\n      invalid: [\n        {\n          angular: `<img class=\"hover:desktop:text-white\" />`,\n          angularOutput: `<img class=\"desktop:hover:text-white\" />`,\n          astro: `<img class=\"hover:desktop:text-white\" />`,\n          astroOutput: `<img class=\"desktop:hover:text-white\" />`,\n          html: `<img class=\"hover:desktop:text-white\" />`,\n          htmlOutput: `<img class=\"desktop:hover:text-white\" />`,\n          jsx: `() => <img class=\"hover:desktop:text-white\" />`,\n          jsxOutput: `() => <img class=\"desktop:hover:text-white\" />`,\n          svelte: `<img class=\"hover:desktop:text-white\" />`,\n          svelteOutput: `<img class=\"desktop:hover:text-white\" />`,\n          vue: `<template><img class=\"hover:desktop:text-white\" /></template>`,\n          vueOutput: `<template><img class=\"desktop:hover:text-white\" /></template>`,\n\n          errors: 1,\n\n          files: {\n            \"styles.css\": `\n              @import \"tailwindcss\";\n\n              @theme {\n                --breakpoint-desktop: 80rem;\n              }\n            `\n          },\n\n          options: [{ entryPoint: \"styles.css\" }]\n        }\n      ]\n    });\n  });\n\n  it(\"should ignore unknown variants\", () => {\n    lint(enforceConsistentVariantOrder, {\n      valid: [\n        {\n          angular: `<img class=\"hover:unknown-variant:text-red-500\" />`,\n          astro: `<img class=\"hover:unknown-variant:text-red-500\" />`,\n          html: `<img class=\"hover:unknown-variant:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:unknown-variant:text-red-500\" />`,\n          svelte: `<img class=\"hover:unknown-variant:text-red-500\" />`,\n          vue: `<template><img class=\"hover:unknown-variant:text-red-500\" /></template>`\n        },\n        {\n          angular: `<img class=\"unknown-variant:hover:text-red-500\" />`,\n          astro: `<img class=\"unknown-variant:hover:text-red-500\" />`,\n          html: `<img class=\"unknown-variant:hover:text-red-500\" />`,\n          jsx: `() => <img class=\"unknown-variant:hover:text-red-500\" />`,\n          svelte: `<img class=\"unknown-variant:hover:text-red-500\" />`,\n          vue: `<template><img class=\"unknown-variant:hover:text-red-500\" /></template>`\n        }\n      ]\n    });\n  });\n});\n\ndescribe.runIf(getTailwindCSSVersion().major <= 3)(enforceConsistentVariantOrder.name, () => {\n\n  it(\"should not report anything in Tailwind CSS v3\", () => {\n    lint(enforceConsistentVariantOrder, {\n      valid: [\n        {\n          angular: `<img class=\"hover:lg:text-red-500\" />`,\n          astro: `<img class=\"hover:lg:text-red-500\" />`,\n          html: `<img class=\"hover:lg:text-red-500\" />`,\n          jsx: `() => <img class=\"hover:lg:text-red-500\" />`,\n          svelte: `<img class=\"hover:lg:text-red-500\" />`,\n          vue: `<template><img class=\"hover:lg:text-red-500\" /></template>`\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "src/rules/enforce-consistent-variant-order.ts",
    "content": "import { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { createGetVariantOrder, getVariantOrder } from \"better-tailwindcss:tailwindcss/variant-order.js\";\nimport { buildClass } from \"better-tailwindcss:utils/class.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { VARIANT_ORDER_FLAGS } from \"better-tailwindcss:utils/order.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\n\nexport const enforceConsistentVariantOrder = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce a consistent variant order for Tailwind classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-consistent-variant-order.md\",\n  name: \"enforce-consistent-variant-order\",\n  recommended: false,\n\n  messages: {\n    order: \"Incorrect variant order. '{{ className }}' should be '{{ fix }}'.\"\n  },\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n    createGetVariantOrder(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => {\n    if(ctx.version.major <= 3){\n      return;\n    }\n\n    for(const literal of literals){\n      const classes = splitClasses(literal.content);\n      const { dissectedClasses, warnings: dissectedWarnings } = getDissectedClasses(async(ctx), classes);\n      const { variantOrder, warnings } = getVariantOrder(async(ctx), classes);\n      const allWarnings = [...dissectedWarnings, ...warnings];\n\n      lintClasses(ctx, literal, className => {\n        const dissectedClass = dissectedClasses[className];\n\n        if(!dissectedClass?.variants || dissectedClass.variants.length <= 1){\n          return;\n        }\n\n        if(dissectedClass.variants.some(variant => variantOrder[variant] === undefined)){\n          return;\n        }\n\n        const sortedVariants = dissectedClass.variants.toSorted((variantA, variantB) => {\n          return compareVariantOrder(\n            variantOrder[variantA],\n            variantOrder[variantB]\n          );\n        });\n\n        if(dissectedClass.variants.every((value, index) => value === sortedVariants[index])){\n          return false;\n        }\n\n        const fix = buildClass(ctx, {\n          ...dissectedClass,\n          variants: sortedVariants\n        });\n\n        return {\n          data: {\n            className,\n            fix\n          },\n          fix,\n          id: \"order\",\n          warnings: allWarnings\n        } as const;\n      });\n    }\n  }\n});\n\nfunction compareVariantOrder(orderA: number | undefined, orderB: number | undefined): number {\n  if(\n    orderA === orderB ||\n    orderA === undefined ||\n    orderB === undefined ||\n    orderA < VARIANT_ORDER_FLAGS.GLOBAL &&\n    orderB < VARIANT_ORDER_FLAGS.GLOBAL\n  ){\n    return 0;\n  }\n\n  if(orderB > orderA){\n    return +1;\n  }\n\n  return -1;\n}\n"
  },
  {
    "path": "src/rules/enforce-logical-properties.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceLogicalProperties } from \"better-tailwindcss:rules/enforce-logical-properties.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version.js\";\n\n\nconst testCases = [\n  [\"pl-4\", \"ps-4\"],\n  [\"pr-4\", \"pe-4\"],\n  [\"pt-4\", \"pbs-4\"],\n  [\"pb-4\", \"pbe-4\"],\n  [\"ml-4\", \"ms-4\"],\n  [\"mr-4\", \"me-4\"],\n  [\"mt-4\", \"mbs-4\"],\n  [\"mb-4\", \"mbe-4\"],\n\n  [\"scroll-pl-4\", \"scroll-ps-4\"],\n  [\"scroll-pr-4\", \"scroll-pe-4\"],\n  [\"scroll-pt-4\", \"scroll-pbs-4\"],\n  [\"scroll-pb-4\", \"scroll-pbe-4\"],\n  [\"scroll-ml-4\", \"scroll-ms-4\"],\n  [\"scroll-mr-4\", \"scroll-me-4\"],\n  [\"scroll-mt-4\", \"scroll-mbs-4\"],\n  [\"scroll-mb-4\", \"scroll-mbe-4\"],\n\n  [\"left-4\", \"inset-s-4\"],\n  [\"right-4\", \"inset-e-4\"],\n  [\"top-4\", \"inset-bs-4\"],\n  [\"bottom-4\", \"inset-be-4\"],\n\n  [\"border-l\", \"border-s\"],\n  [\"border-r\", \"border-e\"],\n  [\"border-t\", \"border-bs\"],\n  [\"border-b\", \"border-be\"],\n  [\"border-l-2\", \"border-s-2\"],\n  [\"border-r-2\", \"border-e-2\"],\n  [\"border-t-2\", \"border-bs-2\"],\n  [\"border-b-2\", \"border-be-2\"],\n\n  [\"rounded-l\", \"rounded-s\"],\n  [\"rounded-r\", \"rounded-e\"],\n  [\"rounded-l-lg\", \"rounded-s-lg\"],\n  [\"rounded-r-lg\", \"rounded-e-lg\"],\n\n  [\"rounded-tl\", \"rounded-ss\"],\n  [\"rounded-tr\", \"rounded-se\"],\n  [\"rounded-br\", \"rounded-ee\"],\n  [\"rounded-bl\", \"rounded-es\"],\n  [\"rounded-tl-lg\", \"rounded-ss-lg\"],\n  [\"rounded-tr-lg\", \"rounded-se-lg\"],\n  [\"rounded-br-lg\", \"rounded-ee-lg\"],\n  [\"rounded-bl-lg\", \"rounded-es-lg\"],\n\n  [\"text-left\", \"text-start\"],\n  [\"text-right\", \"text-end\"],\n\n  [\"float-left\", \"float-start\"],\n  [\"float-right\", \"float-end\"],\n  [\"clear-left\", \"clear-start\"],\n  [\"clear-right\", \"clear-end\"],\n\n  [\"h-4\", \"block-4\"],\n  [\"w-4\", \"inline-4\"],\n  [\"min-h-4\", \"min-block-4\"],\n  [\"min-w-4\", \"min-inline-4\"],\n  [\"max-h-4\", \"max-block-4\"],\n  [\"max-w-4\", \"max-inline-4\"]\n] satisfies [string, string][];\n\ndescribe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name, () => {\n\n  it(\"should not report valid classes\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        valid: [\n          {\n            angular: `<img class=\"ps-4 pbs-2 ms-2 mbs-1 text-start float-start clear-end scroll-ps-2 scroll-pbs-2 inset-bs-2 border-bs rounded-bs\" />`,\n            html: `<img class=\"ps-4 pbs-2 ms-2 mbs-1 text-start float-start clear-end scroll-ps-2 scroll-pbs-2 inset-bs-2 border-bs rounded-bs\" />`,\n            jsx: `() => <img class=\"ps-4 pbs-2 ms-2 mbs-1 text-start float-start clear-end scroll-ps-2 scroll-pbs-2 inset-bs-2 border-bs rounded-bs\" />`,\n            svelte: `<img class=\"ps-4 pbs-2 ms-2 mbs-1 text-start float-start clear-end scroll-ps-2 scroll-pbs-2 inset-bs-2 border-bs rounded-bs\" />`,\n            vue: `<template><img class=\"ps-4 pbs-2 ms-2 mbs-1 text-start float-start clear-end scroll-ps-2 scroll-pbs-2 inset-bs-2 border-bs rounded-bs\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should fix classes with variants\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"hover:pl-4 md:right-2\" />`,\n            angularOutput: `<img class=\"hover:ps-4 md:inset-e-2\" />`,\n            html: `<img class=\"hover:pl-4 md:right-2\" />`,\n            htmlOutput: `<img class=\"hover:ps-4 md:inset-e-2\" />`,\n            jsx: `() => <img class=\"hover:pl-4 md:right-2\" />`,\n            jsxOutput: `() => <img class=\"hover:ps-4 md:inset-e-2\" />`,\n            svelte: `<img class=\"hover:pl-4 md:right-2\" />`,\n            svelteOutput: `<img class=\"hover:ps-4 md:inset-e-2\" />`,\n            vue: `<template><img class=\"hover:pl-4 md:right-2\" /></template>`,\n            vueOutput: `<template><img class=\"hover:ps-4 md:inset-e-2\" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep the important modifier\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!text-left\" />`,\n            angularOutput: `<img class=\"!text-start\" />`,\n            html: `<img class=\"!text-left\" />`,\n            htmlOutput: `<img class=\"!text-start\" />`,\n            jsx: `() => <img class=\"!text-left\" />`,\n            jsxOutput: `() => <img class=\"!text-start\" />`,\n            svelte: `<img class=\"!text-left\" />`,\n            svelteOutput: `<img class=\"!text-start\" />`,\n            vue: `<template><img class=\"!text-left\" /></template>`,\n            vueOutput: `<template><img class=\"!text-start\" /></template>`,\n\n            errors: 1\n          },\n          {\n            angular: `<img class=\"text-right!\" />`,\n            angularOutput: `<img class=\"text-end!\" />`,\n            html: `<img class=\"text-right!\" />`,\n            htmlOutput: `<img class=\"text-end!\" />`,\n            jsx: `() => <img class=\"text-right!\" />`,\n            jsxOutput: `() => <img class=\"text-end!\" />`,\n            svelte: `<img class=\"text-right!\" />`,\n            svelteOutput: `<img class=\"text-end!\" />`,\n            vue: `<template><img class=\"text-right!\" /></template>`,\n            vueOutput: `<template><img class=\"text-end!\" /></template>`,\n\n            errors: 1\n          },\n          {\n            angular: `<img class=\"size-4!\" />`,\n            angularOutput: `<img class=\"block-4! inline-4!\" />`,\n            html: `<img class=\"size-4!\" />`,\n            htmlOutput: `<img class=\"block-4! inline-4!\" />`,\n            jsx: `() => <img class=\"size-4!\" />`,\n            jsxOutput: `() => <img class=\"block-4! inline-4!\" />`,\n            svelte: `<img class=\"size-4!\" />`,\n            svelteOutput: `<img class=\"block-4! inline-4!\" />`,\n            vue: `<template><img class=\"size-4!\" /></template>`,\n            vueOutput: `<template><img class=\"block-4! inline-4!\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep negative classes\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"-ml-4 -right-2\" />`,\n            angularOutput: `<img class=\"-ms-4 -inset-e-2\" />`,\n            html: `<img class=\"-ml-4 -right-2\" />`,\n            htmlOutput: `<img class=\"-ms-4 -inset-e-2\" />`,\n            jsx: `() => <img class=\"-ml-4 -right-2\" />`,\n            jsxOutput: `() => <img class=\"-ms-4 -inset-e-2\" />`,\n            svelte: `<img class=\"-ml-4 -right-2\" />`,\n            svelteOutput: `<img class=\"-ms-4 -inset-e-2\" />`,\n            vue: `<template><img class=\"-ml-4 -right-2\" /></template>`,\n            vueOutput: `<template><img class=\"-ms-4 -inset-e-2\" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep arbitrary values\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"ml-[12px] left-[calc(100%-1rem)]\" />`,\n            angularOutput: `<img class=\"ms-[12px] inset-s-[calc(100%-1rem)]\" />`,\n            html: `<img class=\"ml-[12px] left-[calc(100%-1rem)]\" />`,\n            htmlOutput: `<img class=\"ms-[12px] inset-s-[calc(100%-1rem)]\" />`,\n            jsx: `() => <img class=\"ml-[12px] left-[calc(100%-1rem)]\" />`,\n            jsxOutput: `() => <img class=\"ms-[12px] inset-s-[calc(100%-1rem)]\" />`,\n            svelte: `<img class=\"ml-[12px] left-[calc(100%-1rem)]\" />`,\n            svelteOutput: `<img class=\"ms-[12px] inset-s-[calc(100%-1rem)]\" />`,\n            vue: `<template><img class=\"ml-[12px] left-[calc(100%-1rem)]\" /></template>`,\n            vueOutput: `<template><img class=\"ms-[12px] inset-s-[calc(100%-1rem)]\" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep the tailwindcss prefix\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw:pl-4 tw:text-right\" />`,\n            angularOutput: `<img class=\"tw:ps-4 tw:text-end\" />`,\n            html: `<img class=\"tw:pl-4 tw:text-right\" />`,\n            htmlOutput: `<img class=\"tw:ps-4 tw:text-end\" />`,\n            jsx: `() => <img class=\"tw:pl-4 tw:text-right\" />`,\n            jsxOutput: `() => <img class=\"tw:ps-4 tw:text-end\" />`,\n            svelte: `<img class=\"tw:pl-4 tw:text-right\" />`,\n            svelteOutput: `<img class=\"tw:ps-4 tw:text-end\" />`,\n            vue: `<template><img class=\"tw:pl-4 tw:text-right\" /></template>`,\n            vueOutput: `<template><img class=\"tw:ps-4 tw:text-end\" /></template>`,\n\n            errors: 2,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.each(testCases)(`should report \"%s\"`, (input, output) => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"${input}\" />`,\n            angularOutput: `<img class=\"${output}\" />`,\n            html: `<img class=\"${input}\" />`,\n            htmlOutput: `<img class=\"${output}\" />`,\n            jsx: `() => <img class=\"${input}\" />`,\n            jsxOutput: `() => <img class=\"${output}\" />`,\n            svelte: `<img class=\"${input}\" />`,\n            svelteOutput: `<img class=\"${output}\" />`,\n            vue: `<template><img class=\"${input}\" /></template>`,\n            vueOutput: `<template><img class=\"${output}\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should split size classes into logical block and inline classes\", () => {\n    lint(\n      enforceLogicalProperties,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"size-4 hover:size-[12px]\" />`,\n            angularOutput: `<img class=\"block-4 inline-4 hover:block-[12px] hover:inline-[12px]\" />`,\n            html: `<img class=\"size-4 hover:size-[12px]\" />`,\n            htmlOutput: `<img class=\"block-4 inline-4 hover:block-[12px] hover:inline-[12px]\" />`,\n            jsx: `() => <img class=\"size-4 hover:size-[12px]\" />`,\n            jsxOutput: `() => <img class=\"block-4 inline-4 hover:block-[12px] hover:inline-[12px]\" />`,\n            svelte: `<img class=\"size-4 hover:size-[12px]\" />`,\n            svelteOutput: `<img class=\"block-4 inline-4 hover:block-[12px] hover:inline-[12px]\" />`,\n            vue: `<template><img class=\"size-4 hover:size-[12px]\" /></template>`,\n            vueOutput: `<template><img class=\"block-4 inline-4 hover:block-[12px] hover:inline-[12px]\" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n});\n"
  },
  {
    "path": "src/rules/enforce-logical-properties.ts",
    "content": "import { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { createGetUnknownClasses, getUnknownClasses } from \"better-tailwindcss:tailwindcss/unknown-classes.js\";\nimport { buildClass } from \"better-tailwindcss:utils/class.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { replacePlaceholders, splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const enforceLogicalProperties = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce logical property class names instead of physical directions.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-logical-properties.md\",\n  name: \"enforce-logical-properties\",\n  recommended: false,\n\n  messages: {\n    multiple: \"Physical class detected. Replace \\\"{{ className }}\\\" with logical classes \\\"{{fix}}\\\".\",\n    single: \"Physical class detected. Replace \\\"{{ className }}\\\" with logical class \\\"{{fix}}\\\".\"\n  },\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n    createGetUnknownClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\nconst mappings = [\n  [/^pl-(.*)$/, \"ps-$1\"],\n  [/^pr-(.*)$/, \"pe-$1\"],\n  [/^pt-(.*)$/, \"pbs-$1\"],\n  [/^pb-(.*)$/, \"pbe-$1\"],\n  [/^ml-(.*)$/, \"ms-$1\"],\n  [/^mr-(.*)$/, \"me-$1\"],\n  [/^mt-(.*)$/, \"mbs-$1\"],\n  [/^mb-(.*)$/, \"mbe-$1\"],\n  [/^scroll-ml-(.*)$/, \"scroll-ms-$1\"],\n  [/^scroll-mr-(.*)$/, \"scroll-me-$1\"],\n  [/^scroll-pl-(.*)$/, \"scroll-ps-$1\"],\n  [/^scroll-pr-(.*)$/, \"scroll-pe-$1\"],\n  [/^scroll-mt-(.*)$/, \"scroll-mbs-$1\"],\n  [/^scroll-mb-(.*)$/, \"scroll-mbe-$1\"],\n  [/^scroll-pt-(.*)$/, \"scroll-pbs-$1\"],\n  [/^scroll-pb-(.*)$/, \"scroll-pbe-$1\"],\n\n  [/^left-(.*)$/, \"inset-s-$1\"],\n  [/^right-(.*)$/, \"inset-e-$1\"],\n  [/^top-(.*)$/, \"inset-bs-$1\"],\n  [/^bottom-(.*)$/, \"inset-be-$1\"],\n\n  [/^border-l$/, \"border-s\"],\n  [/^border-l-(.*)$/, \"border-s-$1\"],\n  [/^border-r$/, \"border-e\"],\n  [/^border-r-(.*)$/, \"border-e-$1\"],\n  [/^border-t$/, \"border-bs\"],\n  [/^border-t-(.*)$/, \"border-bs-$1\"],\n  [/^border-b$/, \"border-be\"],\n  [/^border-b-(.*)$/, \"border-be-$1\"],\n\n  [/^rounded-l$/, \"rounded-s\"],\n  [/^rounded-l-(.*)$/, \"rounded-s-$1\"],\n  [/^rounded-r$/, \"rounded-e\"],\n  [/^rounded-r-(.*)$/, \"rounded-e-$1\"],\n\n  [/^rounded-tl$/, \"rounded-ss\"],\n  [/^rounded-tl-(.*)$/, \"rounded-ss-$1\"],\n  [/^rounded-tr$/, \"rounded-se\"],\n  [/^rounded-tr-(.*)$/, \"rounded-se-$1\"],\n  [/^rounded-br$/, \"rounded-ee\"],\n  [/^rounded-br-(.*)$/, \"rounded-ee-$1\"],\n  [/^rounded-bl$/, \"rounded-es\"],\n  [/^rounded-bl-(.*)$/, \"rounded-es-$1\"],\n\n  [/^text-left$/, \"text-start\"],\n  [/^text-right$/, \"text-end\"],\n\n  [/^float-left$/, \"float-start\"],\n  [/^float-right$/, \"float-end\"],\n  [/^clear-left$/, \"clear-start\"],\n  [/^clear-right$/, \"clear-end\"],\n\n  [/^h-(.*)$/, \"block-$1\"],\n  [/^w-(.*)$/, \"inline-$1\"],\n  [/^min-h-(.*)$/, \"min-block-$1\"],\n  [/^min-w-(.*)$/, \"min-inline-$1\"],\n  [/^max-h-(.*)$/, \"max-block-$1\"],\n  [/^max-w-(.*)$/, \"max-inline-$1\"],\n  [/^size-(.*)$/, [\"block-$1\", \"inline-$1\"]]\n] satisfies [before: RegExp, after: string[] | string][];\n\n\nfunction lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: Literal[]) {\n  for(const literal of literals){\n    const classes = splitClasses(literal.content);\n\n    const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);\n\n    const possibleFixes = Object.values(dissectedClasses).flatMap(dissectedClass => {\n      const replacementBases = getReplacementBases(dissectedClass.base);\n\n      if(!replacementBases){\n        return [];\n      }\n\n      return replacementBases.map(base => buildClass(ctx, { ...dissectedClass, base }));\n    });\n\n    const { unknownClasses } = getUnknownClasses(async(ctx), possibleFixes);\n\n    lintClasses(ctx, literal, className => {\n      const dissectedClass = dissectedClasses[className];\n\n      if(!dissectedClass){\n        return;\n      }\n\n      const replacementBases = getReplacementBases(dissectedClass.base);\n\n      if(!replacementBases){\n        return;\n      }\n\n      const fixClasses = replacementBases.map(base => buildClass(ctx, { ...dissectedClass, base }));\n      const hasUnknownFix = fixClasses.some(fixClass => unknownClasses.includes(fixClass));\n\n      if(hasUnknownFix){\n        return;\n      }\n\n      const fix = fixClasses.join(\" \");\n      const id = fixClasses.length > 1 ? \"multiple\" : \"single\";\n\n      return {\n        data: {\n          className,\n          fix\n        },\n        fix,\n        id,\n        warnings\n      } as const;\n    });\n  }\n}\n\nfunction getReplacementBases(base: string): string[] | undefined {\n  for(const [pattern, replacement] of mappings){\n    const match = base.match(pattern);\n\n    if(!match){\n      continue;\n    }\n\n    return Array.isArray(replacement)\n      ? replacement.map(part => replacePlaceholders(part, match))\n      : [replacePlaceholders(replacement, match)];\n  }\n}\n"
  },
  {
    "path": "src/rules/enforce-shorthand-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { enforceShorthandClasses } from \"better-tailwindcss:rules/enforce-shorthand-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css, ts } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\n\n\ndescribe(enforceShorthandClasses.name, () => {\n\n  it(\"should not report on shorthand classes\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"size-4\" />`,\n            html: `<img class=\"size-4\" />`,\n            jsx: `() => <img class=\"size-4\" />`,\n            svelte: `<img class=\"size-4\" />`,\n            vue: `<template><img class=\"size-4\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report on classes that have no shorthand\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"flex\" />`,\n            html: `<img class=\"flex\" />`,\n            jsx: `() => <img class=\"flex\" />`,\n            svelte: `<img class=\"flex\" />`,\n            vue: `<template><img class=\"flex\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should collapse multiple classes into their shorthand\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"w-4 h-4\" />`,\n            angularOutput: `<img class=\"size-4 \" />`,\n            html: `<img class=\"w-4 h-4\" />`,\n            htmlOutput: `<img class=\"size-4 \" />`,\n            jsx: `() => <img class=\"w-4 h-4\" />`,\n            jsxOutput: `() => <img class=\"size-4 \" />`,\n            svelte: `<img class=\"w-4 h-4\" />`,\n            svelteOutput: `<img class=\"size-4 \" />`,\n            vue: `<template><img class=\"w-4 h-4\" /></template>`,\n            vueOutput: `<template><img class=\"size-4 \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not collapse classes if they have a different sign\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-4 -h-4\" />`,\n            html: `<img class=\"w-4 -h-4\" />`,\n            jsx: `() => <img class=\"w-4 -h-4\" />`,\n            svelte: `<img class=\"w-4 -h-4\" />`,\n            vue: `<template><img class=\"w-4 -h-4\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should collapse many classes into their shorthand\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"pt-4 pr-4 pb-4 pl-4\" />`,\n            angularOutput: `<img class=\"p-4   \" />`,\n            html: `<img class=\"pt-4 pr-4 pb-4 pl-4\" />`,\n            htmlOutput: `<img class=\"p-4   \" />`,\n            jsx: `() => <img class=\"pt-4 pr-4 pb-4 pl-4\" />`,\n            jsxOutput: `() => <img class=\"p-4   \" />`,\n            svelte: `<img class=\"pt-4 pr-4 pb-4 pl-4\" />`,\n            svelteOutput: `<img class=\"p-4   \" />`,\n            vue: `<template><img class=\"pt-4 pr-4 pb-4 pl-4\" /></template>`,\n            vueOutput: `<template><img class=\"p-4   \" /></template>`,\n\n            errors: 4\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not collapse differing classes into an otherwise valid shorthand\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"pt-4 pr-2 pb-4 pl-4\" />`,\n            angularOutput: `<img class=\"py-4 pr-2  pl-4\" />`,\n            html: `<img class=\"pt-4 pr-2 pb-4 pl-4\" />`,\n            htmlOutput: `<img class=\"py-4 pr-2  pl-4\" />`,\n            jsx: `() => <img class=\"pt-4 pr-2 pb-4 pl-4\" />`,\n            jsxOutput: `() => <img class=\"py-4 pr-2  pl-4\" />`,\n            svelte: `<img class=\"pt-4 pr-2 pb-4 pl-4\" />`,\n            svelteOutput: `<img class=\"py-4 pr-2  pl-4\" />`,\n            vue: `<template><img class=\"pt-4 pr-2 pb-4 pl-4\" /></template>`,\n            vueOutput: `<template><img class=\"py-4 pr-2  pl-4\" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should work with variants\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"hover:w-4 hover:h-4\" />`,\n            angularOutput: `<img class=\"hover:size-4 \" />`,\n            html: `<img class=\"hover:w-4 hover:h-4\" />`,\n            htmlOutput: `<img class=\"hover:size-4 \" />`,\n            jsx: `() => <img class=\"hover:w-4 hover:h-4\" />`,\n            jsxOutput: `() => <img class=\"hover:size-4 \" />`,\n            svelte: `<img class=\"hover:w-4 hover:h-4\" />`,\n            svelteOutput: `<img class=\"hover:size-4 \" />`,\n            vue: `<template><img class=\"hover:w-4 hover:h-4\" /></template>`,\n            vueOutput: `<template><img class=\"hover:size-4 \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report when one instance has a variant\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-4 hover:h-4\" />`,\n            html: `<img class=\"w-4 hover:h-4\" />`,\n            jsx: `() => <img class=\"w-4 hover:h-4\" />`,\n            svelte: `<img class=\"w-4 hover:h-4\" />`,\n            vue: `<template><img class=\"w-4 hover:h-4\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report when one instance has a different variant\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"focus:w-4 hover:h-4\" />`,\n            html: `<img class=\"focus:w-4 hover:h-4\" />`,\n            jsx: `() => <img class=\"focus:w-4 hover:h-4\" />`,\n            svelte: `<img class=\"focus:w-4 hover:h-4\" />`,\n            vue: `<template><img class=\"focus:w-4 hover:h-4\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should handle arbitrary values correctly\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"w-[10px] h-[10px]\" />`,\n            angularOutput: `<img class=\"size-[10px] \" />`,\n            html: `<img class=\"w-[10px] h-[10px]\" />`,\n            htmlOutput: `<img class=\"size-[10px] \" />`,\n            jsx: `() => <img class=\"w-[10px] h-[10px]\" />`,\n            jsxOutput: `() => <img class=\"size-[10px] \" />`,\n            svelte: `<img class=\"w-[10px] h-[10px]\" />`,\n            svelteOutput: `<img class=\"size-[10px] \" />`,\n            vue: `<template><img class=\"w-[10px] h-[10px]\" /></template>`,\n            vueOutput: `<template><img class=\"size-[10px] \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not collapse arbitrary values with different values\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-[10px] h-[20px]\" />`,\n            html: `<img class=\"w-[10px] h-[20px]\" />`,\n            jsx: `() => <img class=\"w-[10px] h-[20px]\" />`,\n            svelte: `<img class=\"w-[10px] h-[20px]\" />`,\n            vue: `<template><img class=\"w-[10px] h-[20px]\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should handle complex variants correctly\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"sm:hover:w-4 sm:hover:h-4\" />`,\n            angularOutput: `<img class=\"sm:hover:size-4 \" />`,\n            html: `<img class=\"sm:hover:w-4 sm:hover:h-4\" />`,\n            htmlOutput: `<img class=\"sm:hover:size-4 \" />`,\n            jsx: `() => <img class=\"sm:hover:w-4 sm:hover:h-4\" />`,\n            jsxOutput: `() => <img class=\"sm:hover:size-4 \" />`,\n            svelte: `<img class=\"sm:hover:w-4 sm:hover:h-4\" />`,\n            svelteOutput: `<img class=\"sm:hover:size-4 \" />`,\n            vue: `<template><img class=\"sm:hover:w-4 sm:hover:h-4\" /></template>`,\n            vueOutput: `<template><img class=\"sm:hover:size-4 \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not shorten mixed positive and negative values\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"ml-4 -mr-4 mt-4 mb-4\" />`,\n            angularOutput: `<img class=\"ml-4 -mr-4 my-4 \" />`,\n            html: `<img class=\"ml-4 -mr-4 mt-4 mb-4\" />`,\n            htmlOutput: `<img class=\"ml-4 -mr-4 my-4 \" />`,\n            jsx: `() => <img class=\"ml-4 -mr-4 mt-4 mb-4\" />`,\n            jsxOutput: `() => <img class=\"ml-4 -mr-4 my-4 \" />`,\n            svelte: `<img class=\"ml-4 -mr-4 mt-4 mb-4\" />`,\n            svelteOutput: `<img class=\"ml-4 -mr-4 my-4 \" />`,\n            vue: `<template><img class=\"ml-4 -mr-4 mt-4 mb-4\" /></template>`,\n            vueOutput: `<template><img class=\"ml-4 -mr-4 my-4 \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not shorten mixed important and non important classes in tailwind <= 3\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"!w-4 h-4\" />`,\n            html: `<img class=\"!w-4 h-4\" />`,\n            jsx: `() => <img class=\"!w-4 h-4\" />`,\n            svelte: `<img class=\"!w-4 h-4\" />`,\n            vue: `<template><img class=\"!w-4 h-4\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not shorten mixed important and non important classes in tailwind >= 4\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-4! h-4\" />`,\n            html: `<img class=\"w-4! h-4\" />`,\n            jsx: `() => <img class=\"w-4! h-4\" />`,\n            svelte: `<img class=\"w-4! h-4\" />`,\n            vue: `<template><img class=\"w-4! h-4\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should still shorten mixed starting and ending important classes in tailwind >= 4\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"w-4! !h-4\" />`,\n            angularOutput: `<img class=\"size-4! \" />`,\n            html: `<img class=\"w-4! !h-4\" />`,\n            htmlOutput: `<img class=\"size-4! \" />`,\n            jsx: `() => <img class=\"w-4! !h-4\" />`,\n            jsxOutput: `() => <img class=\"size-4! \" />`,\n            svelte: `<img class=\"w-4! !h-4\" />`,\n            svelteOutput: `<img class=\"size-4! \" />`,\n            vue: `<template><img class=\"w-4! !h-4\" /></template>`,\n            vueOutput: `<template><img class=\"size-4! \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should keep important at the start if all classes have important at the start in tailwind >= 4\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!w-4 !h-4\" />`,\n            angularOutput: `<img class=\"!size-4 \" />`,\n            html: `<img class=\"!w-4 !h-4\" />`,\n            htmlOutput: `<img class=\"!size-4 \" />`,\n            jsx: `() => <img class=\"!w-4 !h-4\" />`,\n            jsxOutput: `() => <img class=\"!size-4 \" />`,\n            svelte: `<img class=\"!w-4 !h-4\" />`,\n            svelteOutput: `<img class=\"!size-4 \" />`,\n            vue: `<template><img class=\"!w-4 !h-4\" /></template>`,\n            vueOutput: `<template><img class=\"!size-4 \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should shorten when all classes are important in tailwind <= 3\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"!w-4 !h-4\" />`,\n            angularOutput: `<img class=\"!size-4 \" />`,\n            html: `<img class=\"!w-4 !h-4\" />`,\n            htmlOutput: `<img class=\"!size-4 \" />`,\n            jsx: `() => <img class=\"!w-4 !h-4\" />`,\n            jsxOutput: `() => <img class=\"!size-4 \" />`,\n            svelte: `<img class=\"!w-4 !h-4\" />`,\n            svelteOutput: `<img class=\"!size-4 \" />`,\n            vue: `<template><img class=\"!w-4 !h-4\" /></template>`,\n            vueOutput: `<template><img class=\"!size-4 \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should shorten when all classes are important in tailwind >= 4\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"w-4! h-4!\" />`,\n            angularOutput: `<img class=\"size-4! \" />`,\n            html: `<img class=\"w-4! h-4!\" />`,\n            htmlOutput: `<img class=\"size-4! \" />`,\n            jsx: `() => <img class=\"w-4! h-4!\" />`,\n            jsxOutput: `() => <img class=\"size-4! \" />`,\n            svelte: `<img class=\"w-4! h-4!\" />`,\n            svelteOutput: `<img class=\"size-4! \" />`,\n            vue: `<template><img class=\"w-4! h-4!\" /></template>`,\n            vueOutput: `<template><img class=\"size-4! \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with prefixed tailwind classes in tailwind <= 3\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw-w-full tw-h-full\" />`,\n            angularOutput: `<img class=\"tw-size-full \" />`,\n            html: `<img class=\"tw-w-full tw-h-full\" />`,\n            htmlOutput: `<img class=\"tw-size-full \" />`,\n            jsx: `() => <img class=\"tw-w-full tw-h-full\" />`,\n            jsxOutput: `() => <img class=\"tw-size-full \" />`,\n            svelte: `<img class=\"tw-w-full tw-h-full\" />`,\n            svelteOutput: `<img class=\"tw-size-full \" />`,\n            vue: `<template><img class=\"tw-w-full tw-h-full\" /></template>`,\n            vueOutput: `<template><img class=\"tw-size-full \" /></template>`,\n\n            errors: 2,\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          },\n          {\n            angular: `<img class=\"hover:tw-w-full hover:tw-h-full\" />`,\n            angularOutput: `<img class=\"hover:tw-size-full \" />`,\n            html: `<img class=\"hover:tw-w-full hover:tw-h-full\" />`,\n            htmlOutput: `<img class=\"hover:tw-size-full \" />`,\n            jsx: `() => <img class=\"hover:tw-w-full hover:tw-h-full\" />`,\n            jsxOutput: `() => <img class=\"hover:tw-size-full \" />`,\n            svelte: `<img class=\"hover:tw-w-full hover:tw-h-full\" />`,\n            svelteOutput: `<img class=\"hover:tw-size-full \" />`,\n            vue: `<template><img class=\"hover:tw-w-full hover:tw-h-full\" /></template>`,\n            vueOutput: `<template><img class=\"hover:tw-size-full \" /></template>`,\n\n            errors: 2,\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with prefixed tailwind classes in tailwind >= 4\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw:w-full tw:h-full\" />`,\n            angularOutput: `<img class=\"tw:size-full \" />`,\n            html: `<img class=\"tw:w-full tw:h-full\" />`,\n            htmlOutput: `<img class=\"tw:size-full \" />`,\n            jsx: `() => <img class=\"tw:w-full tw:h-full\" />`,\n            jsxOutput: `() => <img class=\"tw:size-full \" />`,\n            svelte: `<img class=\"tw:w-full tw:h-full\" />`,\n            svelteOutput: `<img class=\"tw:size-full \" />`,\n            vue: `<template><img class=\"tw:w-full tw:h-full\" /></template>`,\n            vueOutput: `<template><img class=\"tw:size-full \" /></template>`,\n\n            errors: 2,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw:hover:w-full tw:hover:h-full\" />`,\n            angularOutput: `<img class=\"tw:hover:size-full \" />`,\n            html: `<img class=\"tw:hover:w-full tw:hover:h-full\" />`,\n            htmlOutput: `<img class=\"tw:hover:size-full \" />`,\n            jsx: `() => <img class=\"tw:hover:w-full tw:hover:h-full\" />`,\n            jsxOutput: `() => <img class=\"tw:hover:size-full \" />`,\n            svelte: `<img class=\"tw:hover:w-full tw:hover:h-full\" />`,\n            svelteOutput: `<img class=\"tw:hover:size-full \" />`,\n            vue: `<template><img class=\"tw:hover:w-full tw:hover:h-full\" /></template>`,\n            vueOutput: `<template><img class=\"tw:hover:size-full \" /></template>`,\n\n            errors: 2,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not work if the shorthand class doesn't actually exist <= 3\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-screen h-screen\" />`,\n            html: `<img class=\"w-screen h-screen\" />`,\n            jsx: `() => <img class=\"w-screen h-screen\" />`,\n            svelte: `<img class=\"w-screen h-screen\" />`,\n            vue: `<template><img class=\"w-screen h-screen\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not work if the shorthand class doesn't actually exist in tailwind >= 4\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"w-screen h-screen\" />`,\n            html: `<img class=\"w-screen h-screen\" />`,\n            jsx: `() => <img class=\"w-screen h-screen\" />`,\n            svelte: `<img class=\"w-screen h-screen\" />`,\n            vue: `<template><img class=\"w-screen h-screen\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not add an additional class if the shorthand class is already present\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"size-4 w-4 h-4\" />`,\n            angularOutput: `<img class=\"size-4  \" />`,\n            html: `<img class=\"size-4 w-4 h-4\" />`,\n            htmlOutput: `<img class=\"size-4  \" />`,\n            jsx: `() => <img class=\"size-4 w-4 h-4\" />`,\n            jsxOutput: `() => <img class=\"size-4  \" />`,\n            svelte: `<img class=\"size-4 w-4 h-4\" />`,\n            svelteOutput: `<img class=\"size-4  \" />`,\n            vue: `<template><img class=\"size-4 w-4 h-4\" /></template>`,\n            vueOutput: `<template><img class=\"size-4  \" /></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should shorten multiple variants separately\", () => {\n    lint(\n      enforceShorthandClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"w-4 h-4 hover:w-8 hover:h-8\" />`,\n            angularOutput: `<img class=\"size-4  hover:size-8 \" />`,\n            html: `<img class=\"w-4 h-4 hover:w-8 hover:h-8\" />`,\n            htmlOutput: `<img class=\"size-4  hover:size-8 \" />`,\n            jsx: `() => <img class=\"w-4 h-4 hover:w-8 hover:h-8\" />`,\n            jsxOutput: `() => <img class=\"size-4  hover:size-8 \" />`,\n            svelte: `<img class=\"w-4 h-4 hover:w-8 hover:h-8\" />`,\n            svelteOutput: `<img class=\"size-4  hover:size-8 \" />`,\n            vue: `<template><img class=\"w-4 h-4 hover:w-8 hover:h-8\" /></template>`,\n            vueOutput: `<template><img class=\"size-4  hover:size-8 \" /></template>`,\n\n            errors: 4\n          }\n        ]\n      }\n    );\n  });\n\n});\n"
  },
  {
    "path": "src/rules/enforce-shorthand-classes.ts",
    "content": "import { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { createGetUnknownClasses, getUnknownClasses } from \"better-tailwindcss:tailwindcss/unknown-classes.js\";\nimport { buildClass } from \"better-tailwindcss:utils/class.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { replacePlaceholders, splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { DissectedClass, DissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const enforceShorthandClasses = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Enforce shorthand class names instead of longhand class names.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/enforce-shorthand-classes.md\",\n  name: \"enforce-shorthand-classes\",\n  recommended: false,\n\n  messages: {\n    longhand: \"Non shorthand class detected. Expected {{ longhands }} to be {{ shorthands }}\",\n    unnecessary: \"Unnecessary whitespace\"\n  },\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n    createGetUnknownClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\n\nexport type Shorthands = [RegExp[], string[]][][];\n\nexport const shorthands = [\n  [\n    [[/^w-(.*)$/, /^h-(.*)$/], [\"size-$1\"]]\n  ],\n  [\n    [[/^ml-(.*)$/, /^mr-(.*)$/, /^mt-(.*)$/, /^mb-(.*)$/], [\"m-$1\"]],\n    [[/^mx-(.*)$/, /^my-(.*)$/], [\"m-$1\"]],\n    [[/^ms-(.*)$/, /^me-(.*)$/], [\"mx-$1\"]],\n    [[/^ml-(.*)$/, /^mr-(.*)$/], [\"mx-$1\"]],\n    [[/^mt-(.*)$/, /^mb-(.*)$/], [\"my-$1\"]]\n  ],\n  [\n    [[/^pl-(.*)$/, /^pr-(.*)$/, /^pt-(.*)$/, /^pb-(.*)$/], [\"p-$1\"]],\n    [[/^px-(.*)$/, /^py-(.*)$/], [\"p-$1\"]],\n    [[/^ps-(.*)$/, /^pe-(.*)$/], [\"px-$1\"]],\n    [[/^pl-(.*)$/, /^pr-(.*)$/], [\"px-$1\"]],\n    [[/^pt-(.*)$/, /^pb-(.*)$/], [\"py-$1\"]]\n  ],\n  [\n    [[/^border-t-(.*)$/, /^border-b-(.*)$/, /^border-l-(.*)$/, /^border-r-(.*)$/], [\"border-$1\"]],\n    [[/^border-x-(.*)$/, /^border-y-(.*)$/], [\"border-$1\"]],\n    [[/^border-s-(.*)$/, /^border-e-(.*)$/], [\"border-x-$1\"]],\n    [[/^border-l-(.*)$/, /^border-r-(.*)$/], [\"border-x-$1\"]],\n    [[/^border-t-(.*)$/, /^border-b-(.*)$/], [\"border-y-$1\"]]\n  ],\n  [\n    [[/^border-spacing-x-(.*)$/, /^border-spacing-y-(.*)$/], [\"border-spacing-$1\"]]\n  ],\n  [\n    [[/^rounded-tl-(.*)$/, /^rounded-tr-(.*)$/, /^rounded-bl-(.*)$/, /^rounded-br-(.*)$/], [\"rounded-$1\"]],\n    [[/^rounded-t-(.*)$/, /^rounded-b-(.*)$/], [\"rounded-$1\"]],\n    [[/^rounded-l-(.*)$/, /^rounded-r-(.*)$/], [\"rounded-$1\"]],\n    [[/^rounded-tl-(.*)$/, /^rounded-tr-(.*)$/], [\"rounded-t-$1\"]],\n    [[/^rounded-bl-(.*)$/, /^rounded-br-(.*)$/], [\"rounded-b-$1\"]],\n    [[/^rounded-tl-(.*)$/, /^rounded-bl-(.*)$/], [\"rounded-l-$1\"]],\n    [[/^rounded-tr-(.*)$/, /^rounded-br-(.*)$/], [\"rounded-r-$1\"]]\n  ],\n  [\n    [[/^scroll-mt-(.*)$/, /^scroll-mb-(.*)$/, /^scroll-ml-(.*)$/, /^scroll-mr-(.*)$/], [\"scroll-m-$1\"]],\n    [[/^scroll-mx-(.*)$/, /^scroll-my-(.*)$/], [\"scroll-m-$1\"]],\n    [[/^scroll-ms-(.*)$/, /^scroll-me-(.*)$/], [\"scroll-mx-$1\"]],\n    [[/^scroll-ml-(.*)$/, /^scroll-mr-(.*)$/], [\"scroll-mx-$1\"]],\n    [[/^scroll-mt-(.*)$/, /^scroll-mb-(.*)$/], [\"scroll-my-$1\"]]\n  ],\n  [\n    [[/^scroll-pt-(.*)$/, /^scroll-pb-(.*)$/, /^scroll-pl-(.*)$/, /^scroll-pr-(.*)$/], [\"scroll-p-$1\"]],\n    [[/^scroll-px-(.*)$/, /^scroll-py-(.*)$/], [\"scroll-p-$1\"]],\n    [[/^scroll-pl-(.*)$/, /^scroll-pr-(.*)$/], [\"scroll-px-$1\"]],\n    [[/^scroll-ps-(.*)$/, /^scroll-pe-(.*)$/], [\"scroll-px-$1\"]],\n    [[/^scroll-pt-(.*)$/, /^scroll-pb-(.*)$/], [\"scroll-py-$1\"]]\n  ],\n  [\n    [[/^top-(.*)$/, /^right-(.*)$/, /^bottom-(.*)$/, /^left-(.*)$/], [\"inset-$1\"]],\n    [[/^inset-x-(.*)$/, /^inset-y-(.*)$/], [\"inset-$1\"]]\n  ],\n  [\n    [[/^divide-x-(.*)$/, /^divide-y-(.*)$/], [\"divide-$1\"]]\n  ],\n  [\n    [[/^space-x-(.*)$/, /^space-y-(.*)$/], [\"space-$1\"]]\n  ],\n  [\n    [[/^gap-x-(.*)$/, /^gap-y-(.*)$/], [\"gap-$1\"]]\n  ],\n  [\n    [[/^translate-x-(.*)$/, /^translate-y-(.*)$/], [\"translate-$1\"]]\n  ],\n  [\n    [[/^rotate-x-(.*)$/, /^rotate-y-(.*)$/], [\"rotate-$1\"]]\n  ],\n  [\n    [[/^skew-x-(.*)$/, /^skew-y-(.*)$/], [\"skew-$1\"]]\n  ],\n  [\n    [[/^scale-x-(.*)$/, /^scale-y-(.*)$/, /^scale-z-(.*)$/], [\"scale-$1\", \"scale-3d\"]],\n    [[/^scale-x-(.*)$/, /^scale-y-(.*)$/], [\"scale-$1\"]]\n  ],\n  [\n    [[/^content-(.*)$/, /^justify-content-(.*)$/], [\"place-content-$1\"]],\n    [[/^items-(.*)$/, /^justify-items-(.*)$/], [\"place-items-$1\"]],\n    [[/^self-(.*)$/, /^justify-self-(.*)$/], [\"place-self-$1\"]]\n  ],\n  [\n    [[/^overflow-hidden/, /^text-ellipsis/, /^whitespace-nowrap/], [\"truncate\"]]\n  ]\n] satisfies Shorthands;\n\n\nfunction lintLiterals(ctx: Context<typeof enforceShorthandClasses>, literals: Literal[]) {\n  for(const literal of literals){\n\n    const classes = splitClasses(literal.content);\n    const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);\n\n    const shorthandGroups = getShorthands(ctx, dissectedClasses);\n\n    const { unknownClasses } = getUnknownClasses(\n      async(ctx),\n      shorthandGroups\n        .flat()\n        .flatMap(([, shorthands]) => shorthands)\n        .flat()\n    );\n\n\n    lintClasses(ctx, literal, (className, index, after) => {\n      for(const shorthandGroup of shorthandGroups){\n        for(const [longhands, shorthands] of shorthandGroup){\n\n          const longhandClasses = longhands.map(longhand => buildClass(ctx, longhand));\n\n          if(!longhandClasses.includes(className)){\n            continue;\n          }\n\n          if(shorthands.some(shorthand => unknownClasses.includes(shorthand))){\n            continue;\n          }\n\n          if(shorthands.every(shorthand => after.includes(shorthand))){\n            return {\n              fix: \"\",\n              id: \"unnecessary\"\n            } as const;\n          }\n\n          return {\n            data: {\n              longhands: longhandClasses.join(\" \"),\n              shorthands: shorthands.join(\" \")\n            },\n            fix: shorthands.filter(shorthand => !after.includes(shorthand)).join(\" \"),\n            id: \"longhand\",\n            warnings\n          } as const;\n        }\n      }\n    });\n\n  }\n}\n\nfunction getShorthands(ctx: Context<typeof enforceShorthandClasses>, dissectedClasses: DissectedClasses) {\n\n  const possibleShorthandClassesGroups: [longhands: DissectedClass[], shorthands: string[]][][] = [];\n\n  for(const shorthandGroup of shorthands){\n\n    const sortedShorthandGroup = shorthandGroup.sort((a, b) => b[0].length - a[0].length);\n\n    const possibleShorthandClasses: [longhands: DissectedClass[], shorthands: string[]][] = [];\n\n    shorthandLoop: for(const [patterns, substitutes] of sortedShorthandGroup){\n\n      const groupedByVariants = Object.values(dissectedClasses).reduce<Record<string, DissectedClass[]>>((acc, dissectedClass) => {\n        const variants = dissectedClass.variants?.join(dissectedClass.separator) ?? \"\";\n\n        acc[variants] ??= [];\n        acc[variants].push(dissectedClass);\n\n        return acc;\n      }, {});\n\n      for(const variantGroup in groupedByVariants){\n\n        const longhands: DissectedClass[] = [];\n        const groups: string[] = [];\n\n        for(const pattern of patterns){\n          classNameLoop: for(const dissectedClass of groupedByVariants[variantGroup]){\n            const match = dissectedClass.base.match(pattern);\n\n            if(!match){\n              continue classNameLoop;\n            }\n\n            for(let m = 0; m < match.length; m++){\n              if(groups[m] === undefined){\n                groups[m] = match[m];\n                continue;\n              }\n\n              if(m === 0){\n                continue;\n              }\n\n              if(groups[m] !== match[m]){\n                continue shorthandLoop;\n              }\n            }\n\n            longhands.push(dissectedClass);\n          }\n        }\n\n        const isImportantAtEnd = longhands.some(longhand => longhand.important[1]);\n        const isImportantAtStart = !isImportantAtEnd && longhands.some(longhand => longhand.important[0]);\n\n        const negative = longhands.some(longhand => longhand.negative);\n\n        const prefix = longhands[0]?.prefix ?? \"\";\n        const variants = longhands[0]?.variants;\n        const separator = longhands[0]?.separator ?? \":\";\n\n        if(\n          longhands.length !== patterns.length ||\n          longhands.some(longhand => (longhand?.important[0] || longhand?.important[1]) !== (isImportantAtStart || isImportantAtEnd)) ||\n          longhands.some(longhand => longhand?.negative !== negative) ||\n          longhands.some(longhand => longhand?.variants?.join(separator) !== variants?.join(separator))\n        ){\n          continue;\n        }\n\n        if(longhands.length === patterns.length){\n          possibleShorthandClasses.push([longhands, substitutes.map(substitute => buildClass(ctx, {\n            base: replacePlaceholders(substitute, groups),\n            important: [isImportantAtStart, isImportantAtEnd],\n            negative,\n            prefix,\n            separator,\n            variants\n          }))]);\n        }\n      }\n    }\n\n    if(possibleShorthandClasses.length > 0){\n      possibleShorthandClassesGroups.push(possibleShorthandClasses.sort((a, b) => b[0].length - a[0].length));\n    }\n\n  }\n\n  return possibleShorthandClassesGroups;\n}\n"
  },
  {
    "path": "src/rules/no-conflicting-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noConflictingClasses } from \"better-tailwindcss:rules/no-conflicting-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\n\n\ndescribe.skipIf(getTailwindCSSVersion().major <= 3)(noConflictingClasses.name, () => {\n\n  it(\"should not report on non-conflicting tailwind classes\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"flex\" />`,\n            html: `<img class=\"flex\" />`,\n            jsx: `() => <img class=\"flex\" />`,\n            svelte: `<img class=\"flex\" />`,\n            vue: `<template><img class=\"flex\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should report on conflicting tailwind classes\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        invalid: [\n          {\n            angular: `<div class=\"flex block\"></div>`,\n            html: `<div class=\"flex block\"></div>`,\n            jsx: `() => <div class=\"flex block\"></div>`,\n            svelte: `<div class=\"flex block\"></div>`,\n            vue: `<template><div class=\"flex block\"></div></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report on different variants\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        valid: [\n          {\n            angular: `<div class=\"flex hover:block\"></div>`,\n            html: `<div class=\"flex hover:block\"></div>`,\n            jsx: `() => <div class=\"flex hover:block\"></div>`,\n            svelte: `<div class=\"flex hover:block\"></div>`,\n            vue: `<template><div class=\"flex hover:block\"></div></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report on the variants itself\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        valid: [\n          {\n            angular: `<div class=\"hover:flex hover:font-bold\"></div>`,\n            html: `<div class=\"hover:flex hover:font-bold\"></div>`,\n            jsx: `() => <div class=\"hover:flex hover:font-bold\"></div>`,\n            svelte: `<div class=\"hover:flex hover:font-bold\"></div>`,\n            vue: `<template><div class=\"hover:flex hover:font-bold\"></div></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should report on the same variants\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        invalid: [\n          {\n            angular: `<div class=\"md:hover:flex md:hover:block\"></div>`,\n            html: `<div class=\"hover:flex hover:block\"></div>`,\n            jsx: `() => <div class=\"hover:flex hover:block\"></div>`,\n            svelte: `<div class=\"hover:flex hover:block\"></div>`,\n            vue: `<template><div class=\"hover:flex hover:block\"></div></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should even report on classes if one of them has an important flag\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        invalid: [\n          {\n            angular: `<div class=\"flex block!\"></div>`,\n            html: `<div class=\"flex block!\"></div>`,\n            jsx: `() => <div class=\"flex block!\"></div>`,\n            svelte: `<div class=\"flex block!\"></div>`,\n            vue: `<template><div class=\"flex block!\"></div></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report for css properties with an `undefined` value\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        valid: [\n          {\n            angular: `<div class=\"text-sm font-thin\"></div>`,\n            html: `<div class=\"text-sm font-thin\"></div>`,\n            jsx: `() => <div class=\"text-sm font-thin\"></div>`,\n            svelte: `<div class=\"text-sm font-thin\"></div>`,\n            vue: `<template><div class=\"text-sm font-thin\"></div></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  // #135\n  it(\"should report errors when multiple properties apply the same styles\", () => {\n    lint(\n      noConflictingClasses,\n      {\n        invalid: [\n          {\n            angular: `<div class=\"outline outline-1\"></div>`,\n            html: `<div class=\"outline outline-1\"></div>`,\n            jsx: `() => <div class=\"outline outline-1\"></div>`,\n            svelte: `<div class=\"outline outline-1\"></div>`,\n            vue: `<template><div class=\"outline outline-1\"></div></template>`,\n\n            errors: 2\n          }\n        ]\n      }\n    );\n  });\n});\n"
  },
  {
    "path": "src/rules/no-conflicting-classes.ts",
    "content": "import {\n  createGetConflictingClasses,\n  getConflictingClasses\n} from \"better-tailwindcss:tailwindcss/conflicting-classes.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const noConflictingClasses = createRule({\n  autofix: true,\n  category: \"correctness\",\n  description: \"Disallow classes that produce conflicting styles.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-conflicting-classes.md\",\n  name: \"no-conflicting-classes\",\n  recommended: true,\n\n  messages: {\n    conflicting: \"Conflicting class detected: \\\"{{ className }}\\\" and \\\"{{ conflictingClassString }}\\\" apply the same CSS properties: \\\"{{ conflictingPropertiesString }}\\\".\"\n  },\n\n  initialize(ctx) {\n    createGetConflictingClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\nfunction lintLiterals(ctx: Context<typeof noConflictingClasses>, literals: Literal[]) {\n\n  for(const literal of literals){\n\n    const classes = splitClasses(literal.content);\n\n    const { conflictingClasses, warnings } = getConflictingClasses(async(ctx), classes);\n\n    if(Object.keys(conflictingClasses).length === 0){\n      continue;\n    }\n\n    lintClasses(ctx, literal, className => {\n      if(!conflictingClasses[className]){\n        return;\n      }\n\n      const conflicts = Object.entries(conflictingClasses[className]);\n\n      if(conflicts.length === 0){\n        return;\n      }\n\n      const conflictingClassNames = conflicts.map(([conflictingClassName]) => conflictingClassName);\n      const conflictingProperties = conflicts.reduce<string[]>((acc, [, properties]) => {\n        for(const property of properties){\n          if(!acc.includes(property.cssPropertyName)){\n            acc.push(property.cssPropertyName);\n          }\n        }\n        return acc;\n      }, []);\n\n      const conflictingClassString = conflictingClassNames.join(\", \");\n      const conflictingPropertiesString = conflictingProperties.map(conflictingProperty => `\"${conflictingProperty}\"`).join(\", \");\n\n      return {\n        data: {\n          className,\n          conflictingClassString,\n          conflictingPropertiesString\n        },\n        id: \"conflicting\",\n        warnings\n      } as const;\n\n    });\n\n  }\n}\n"
  },
  {
    "path": "src/rules/no-deprecated-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noDeprecatedClasses } from \"better-tailwindcss:rules/no-deprecated-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\n\n\nconst testCases = [\n  [\"shadow\", \"shadow-sm\"],\n  [\"inset-shadow\", \"inset-shadow-sm\"],\n  [\"drop-shadow\", \"drop-shadow-sm\"],\n  [\"blur\", \"blur-sm\"],\n  [\"backdrop-blur\", \"backdrop-blur-sm\"],\n  [\"rounded\", \"rounded-sm\"],\n\n  [\"bg-opacity-70\", undefined],\n  [\"text-opacity-70\", undefined],\n  [\"border-opacity-70\", undefined],\n  [\"divide-opacity-70\", undefined],\n  [\"ring-opacity-70\", undefined],\n  [\"placeholder-opacity-70\", undefined],\n\n  [\"flex-shrink\", \"shrink\"],\n  [\"flex-shrink-1\", \"shrink-1\"],\n  [\"flex-grow\", \"grow\"],\n  [\"flex-grow-1\", \"grow-1\"],\n\n  [\"overflow-ellipsis\", \"text-ellipsis\"],\n\n  [\"decoration-slice\", \"box-decoration-slice\"],\n  [\"decoration-clone\", \"box-decoration-clone\"],\n\n  // 4.1 deprecations\n  [\"bg-left-top\", \"bg-top-left\"],\n  [\"bg-left-bottom\", \"bg-bottom-left\"],\n  [\"bg-right-top\", \"bg-top-right\"],\n  [\"bg-right-bottom\", \"bg-bottom-right\"],\n  [\"object-left-top\", \"object-top-left\"],\n  [\"object-left-bottom\", \"object-bottom-left\"],\n  [\"object-right-top\", \"object-top-right\"],\n  [\"object-right-bottom\", \"object-bottom-right\"]\n] satisfies [string, string | undefined][];\n\ndescribe.runIf(getTailwindCSSVersion().major >= 4)(noDeprecatedClasses.name, () => {\n\n  it(\"should not report valid classes\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"shadow-sm font-bold bg-black/50 shrink-1 text-ellipsis\" />`,\n            html: `<img class=\"shadow-sm font-bold bg-black/50 shrink-1 text-ellipsis\" />`,\n            jsx: `() => <img class=\"shadow-sm font-bold bg-black/50 shrink-1 text-ellipsis\" />`,\n            svelte: `<img class=\"shadow-sm font-bold bg-black/50 shrink-1 text-ellipsis\" />`,\n            vue: `<template><img class=\"shadow-sm font-bold bg-black/50 shrink-1 text-ellipsis\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should fix replaceable deprecated classes\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"shadow\" />`,\n            angularOutput: `<img class=\"shadow-sm\" />`,\n            html: `<img class=\"shadow\" />`,\n            htmlOutput: `<img class=\"shadow-sm\" />`,\n            jsx: `() => <img class=\"shadow\" />`,\n            jsxOutput: `() => <img class=\"shadow-sm\" />`,\n            svelte: `<img class=\"shadow\" />`,\n            svelteOutput: `<img class=\"shadow-sm\" />`,\n            vue: `<template><img class=\"shadow\" /></template>`,\n            vueOutput: `<template><img class=\"shadow-sm\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should warn for irreplaceable deprecated classes\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"bg-opacity-50\" />`,\n            html: `<img class=\"bg-opacity-50\" />`,\n            jsx: `() => <img class=\"bg-opacity-50\" />`,\n            svelte: `<img class=\"bg-opacity-50\" />`,\n            vue: `<template><img class=\"bg-opacity-50\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should work with variants\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"hover:shadow\" />`,\n            angularOutput: `<img class=\"hover:shadow-sm\" />`,\n            html: `<img class=\"hover:shadow\" />`,\n            htmlOutput: `<img class=\"hover:shadow-sm\" />`,\n            jsx: `() => <img class=\"hover:shadow\" />`,\n            jsxOutput: `() => <img class=\"hover:shadow-sm\" />`,\n            svelte: `<img class=\"hover:shadow\" />`,\n            svelteOutput: `<img class=\"hover:shadow-sm\" />`,\n            vue: `<template><img class=\"hover:shadow\" /></template>`,\n            vueOutput: `<template><img class=\"hover:shadow-sm\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep the original value\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex-shrink-1\" />`,\n            angularOutput: `<img class=\"shrink-1\" />`,\n            html: `<img class=\"flex-shrink-1\" />`,\n            htmlOutput: `<img class=\"shrink-1\" />`,\n            jsx: `() => <img class=\"flex-shrink-1\" />`,\n            jsxOutput: `() => <img class=\"shrink-1\" />`,\n            svelte: `<img class=\"flex-shrink-1\" />`,\n            svelteOutput: `<img class=\"shrink-1\" />`,\n            vue: `<template><img class=\"flex-shrink-1\" /></template>`,\n            vueOutput: `<template><img class=\"shrink-1\" /></template>`,\n\n            errors: 1\n          },\n          {\n            angular: `<img class=\"flex-shrink-[1]\" />`,\n            angularOutput: `<img class=\"shrink-[1]\" />`,\n            html: `<img class=\"flex-shrink-[1]\" />`,\n            htmlOutput: `<img class=\"shrink-[1]\" />`,\n            jsx: `() => <img class=\"flex-shrink-[1]\" />`,\n            jsxOutput: `() => <img class=\"shrink-[1]\" />`,\n            svelte: `<img class=\"flex-shrink-[1]\" />`,\n            svelteOutput: `<img class=\"shrink-[1]\" />`,\n            vue: `<template><img class=\"flex-shrink-[1]\" /></template>`,\n            vueOutput: `<template><img class=\"shrink-[1]\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep the important modifier\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"shadow!\" />`,\n            angularOutput: `<img class=\"shadow-sm!\" />`,\n            html: `<img class=\"shadow!\" />`,\n            htmlOutput: `<img class=\"shadow-sm!\" />`,\n            jsx: `() => <img class=\"shadow!\" />`,\n            jsxOutput: `() => <img class=\"shadow-sm!\" />`,\n            svelte: `<img class=\"shadow!\" />`,\n            svelteOutput: `<img class=\"shadow-sm!\" />`,\n            vue: `<template><img class=\"shadow!\" /></template>`,\n            vueOutput: `<template><img class=\"shadow-sm!\" /></template>`,\n\n            errors: 1\n          },\n          {\n            angular: `<img class=\"!shadow\" />`,\n            angularOutput: `<img class=\"!shadow-sm\" />`,\n            html: `<img class=\"!shadow\" />`,\n            htmlOutput: `<img class=\"!shadow-sm\" />`,\n            jsx: `() => <img class=\"!shadow\" />`,\n            jsxOutput: `() => <img class=\"!shadow-sm\" />`,\n            svelte: `<img class=\"!shadow\" />`,\n            svelteOutput: `<img class=\"!shadow-sm\" />`,\n            vue: `<template><img class=\"!shadow\" /></template>`,\n            vueOutput: `<template><img class=\"!shadow-sm\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should keep the tailwindcss prefix\", () => {\n    lint(\n      noDeprecatedClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"tw:shadow\" />`,\n            angularOutput: `<img class=\"tw:shadow-sm\" />`,\n            html: `<img class=\"tw:shadow\" />`,\n            htmlOutput: `<img class=\"tw:shadow-sm\" />`,\n            jsx: `() => <img class=\"tw:shadow\" />`,\n            jsxOutput: `() => <img class=\"tw:shadow-sm\" />`,\n            svelte: `<img class=\"tw:shadow\" />`,\n            svelteOutput: `<img class=\"tw:shadow-sm\" />`,\n            vue: `<template><img class=\"tw:shadow\" /></template>`,\n            vueOutput: `<template><img class=\"tw:shadow-sm\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw:hover:shadow!\" />`,\n            angularOutput: `<img class=\"tw:hover:shadow-sm!\" />`,\n            html: `<img class=\"tw:hover:shadow!\" />`,\n            htmlOutput: `<img class=\"tw:hover:shadow-sm!\" />`,\n            jsx: `() => <img class=\"tw:hover:shadow!\" />`,\n            jsxOutput: `() => <img class=\"tw:hover:shadow-sm!\" />`,\n            svelte: `<img class=\"tw:hover:shadow!\" />`,\n            svelteOutput: `<img class=\"tw:hover:shadow-sm!\" />`,\n            vue: `<template><img class=\"tw:hover:shadow!\" /></template>`,\n            vueOutput: `<template><img class=\"tw:hover:shadow-sm!\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n\n  });\n\n  it.each(testCases)(`should report \"%s\"`, (input, output) => {\n\n    const hasFix = output !== undefined;\n\n    if(hasFix){\n      lint(\n        noDeprecatedClasses,\n        {\n          invalid: [\n            {\n              angular: `<img class=\"${input}\" />`,\n              angularOutput: `<img class=\"${output}\" />`,\n              html: `<img class=\"${input}\" />`,\n              htmlOutput: `<img class=\"${output}\" />`,\n              jsx: `() => <img class=\"${input}\" />`,\n              jsxOutput: `() => <img class=\"${output}\" />`,\n              svelte: `<img class=\"${input}\" />`,\n              svelteOutput: `<img class=\"${output}\" />`,\n              vue: `<template><img class=\"${input}\" /></template>`,\n              vueOutput: `<template><img class=\"${output}\" /></template>`,\n\n              errors: 1\n            }\n          ]\n        }\n      );\n    } else {\n      lint(\n        noDeprecatedClasses,\n        {\n          invalid: [\n            {\n              angular: `<img class=\"${input}\" />`,\n              html: `<img class=\"${input}\" />`,\n              jsx: `() => <img class=\"${input}\" />`,\n              svelte: `<img class=\"${input}\" />`,\n              vue: `<template><img class=\"${input}\" /></template>`,\n\n              errors: 1\n            }\n          ]\n        }\n      );\n    }\n\n  });\n\n});\n"
  },
  {
    "path": "src/rules/no-deprecated-classes.ts",
    "content": "import { createGetDissectedClasses, getDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { buildClass } from \"better-tailwindcss:utils/class.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { replacePlaceholders, splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const noDeprecatedClasses = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Disallow the use of deprecated Tailwind CSS classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-deprecated-classes.md\",\n  name: \"no-deprecated-classes\",\n  recommended: true,\n\n  messages: {\n    irreplaceable: \"Class \\\"{{ className }}\\\" is deprecated. Check the tailwindcss documentation for more information: https://tailwindcss.com/docs/upgrade-guide#removed-deprecated-utilities\",\n    replaceable: \"Deprecated class detected. Replace \\\"{{ className }}\\\" with \\\"{{fix}}\\\".\"\n  },\n\n  initialize: ctx => {\n    createGetDissectedClasses(ctx);\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\nconst deprecations = [\n  [\n    { major: 4, minor: 0 }, [\n      [/^shadow$/, \"shadow-sm\"],\n      [/^inset-shadow$/, \"inset-shadow-sm\"],\n      [/^drop-shadow$/, \"drop-shadow-sm\"],\n      [/^blur$/, \"blur-sm\"],\n      [/^backdrop-blur$/, \"backdrop-blur-sm\"],\n      [/^rounded$/, \"rounded-sm\"],\n\n      [/^bg-opacity-(.*)$/],\n      [/^text-opacity-(.*)$/],\n      [/^border-opacity-(.*)$/],\n      [/^divide-opacity-(.*)$/],\n      [/^ring-opacity-(.*)$/],\n      [/^placeholder-opacity-(.*)$/],\n\n      [/^flex-shrink$/, \"shrink\"],\n      [/^flex-shrink-(.*)$/, \"shrink-$1\"],\n      [/^flex-grow$/, \"grow\"],\n      [/^flex-grow-(.*)$/, \"grow-$1\"],\n\n      [/^overflow-ellipsis$/, \"text-ellipsis\"],\n\n      [/^decoration-slice$/, \"box-decoration-slice\"],\n      [/^decoration-clone$/, \"box-decoration-clone\"]\n    ]\n  ], [\n    { major: 4, minor: 1 }, [\n      [/^bg-left-top$/, \"bg-top-left\"],\n      [/^bg-left-bottom$/, \"bg-bottom-left\"],\n      [/^bg-right-top$/, \"bg-top-right\"],\n      [/^bg-right-bottom$/, \"bg-bottom-right\"],\n      [/^object-left-top$/, \"object-top-left\"],\n      [/^object-left-bottom$/, \"object-bottom-left\"],\n      [/^object-right-top$/, \"object-top-right\"],\n      [/^object-right-bottom$/, \"object-bottom-right\"]\n    ]\n  ]\n] satisfies [{ major: number; minor: number; }, [before: RegExp, after?: string][]][];\n\n\nfunction lintLiterals(ctx: Context<typeof noDeprecatedClasses>, literals: Literal[]) {\n\n  const { major, minor } = ctx.version;\n\n  for(const literal of literals){\n\n    const classes = splitClasses(literal.content);\n\n    const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);\n\n    lintClasses(ctx, literal, className => {\n      const dissectedClass = dissectedClasses[className];\n\n      if(!dissectedClass){\n        return;\n      }\n\n      for(const [version, deprecation] of deprecations){\n        if(major < version.major || major === version.major && minor < version.minor){\n          continue;\n        }\n\n        for(const [pattern, replacement] of deprecation){\n          const match = dissectedClass.base.match(pattern);\n\n          if(!match){\n            continue;\n          }\n\n          if(!replacement){\n            return {\n              data: {\n                className\n              } as Record<string, string>,\n              id: \"irreplaceable\",\n              warnings\n            } as const;\n          }\n\n          const fix = buildClass(ctx, { ...dissectedClass, base: replacePlaceholders(replacement, match) });\n\n          return {\n            data: {\n              className,\n              fix\n            },\n            fix,\n            id: \"replaceable\",\n            warnings\n          } as const;\n\n        }\n      }\n    });\n\n  }\n}\n"
  },
  {
    "path": "src/rules/no-duplicate-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noDuplicateClasses } from \"better-tailwindcss:rules/no-duplicate-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\n\n\ndescribe(noDuplicateClasses.name, () => {\n\n  it(\"should filter all duplicate classes\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"  b  a  c  a  \" />`,\n          angularOutput: `<img class=\"  b  a  c    \" />`,\n          html: `<img class=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img class=\"  b  a  c    \" />`,\n          jsx: `() => <img class=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img class=\"  b  a  c    \" />`,\n          svelte: `<img class=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img class=\"  b  a  c    \" />`,\n          vue: `<template><img class=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img class=\"  b  a  c    \" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should keep the quotes as they are\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"  b  a  b  \" />`,\n          angularOutput: `<img class=\"  b  a    \" />`,\n          html: `<img class=\"  b  a  b  \" />`,\n          htmlOutput: `<img class=\"  b  a    \" />`,\n          jsx: `() => <img class=\"  b  a  b  \" />`,\n          jsxOutput: `() => <img class=\"  b  a    \" />`,\n          svelte: `<img class=\"  b  a  b  \" />`,\n          svelteOutput: `<img class=\"  b  a    \" />`,\n          vue: `<template><img class=\"  b  a  b  \" /></template>`,\n          vueOutput: `<template><img class=\"  b  a    \" /></template>`,\n\n          errors: 1\n        },\n        {\n          angular: `<img class='  b  a  b  ' />`,\n          angularOutput: `<img class='  b  a    ' />`,\n          html: `<img class='  b  a  b  ' />`,\n          htmlOutput: `<img class='  b  a    ' />`,\n          jsx: `() => <img class='  b  a  b  ' />`,\n          jsxOutput: `() => <img class='  b  a    ' />`,\n          svelte: `<img class='  b  a  b  ' />`,\n          svelteOutput: `<img class='  b  a    ' />`,\n          vue: `<template><img class='  b  a  b  ' /></template>`,\n          vueOutput: `<template><img class='  b  a    ' /></template>`,\n\n          errors: 1\n        },\n        {\n          jsx: `() => <img class={\\`  b  a  b  \\`} />`,\n          jsxOutput: `() => <img class={\\`  b  a    \\`} />`,\n          svelte: `<img class={\\`  b  a  b  \\`} />`,\n          svelteOutput: `<img class={\\`  b  a    \\`} />`,\n\n          errors: 1\n        },\n        {\n          jsx: `() => <img class={\"  b  a  b  \"} />`,\n          jsxOutput: `() => <img class={\"  b  a    \"} />`,\n          svelte: `<img class={\"  b  a  b  \"} />`,\n          svelteOutput: `<img class={\"  b  a    \"} />`,\n\n          errors: 1\n        },\n        {\n          jsx: `() => <img class={'  b  a  b  '} />`,\n          jsxOutput: `() => <img class={'  b  a    '} />`,\n          svelte: `<img class={'  b  a  b  '} />`,\n          svelteOutput: `<img class={'  b  a    '} />`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should remove duplicate classes in multiline strings\", () => {\n\n    const dirty = dedent`\n      b\n      a\n      b\n    `;\n\n    const clean = dedent`\n      b\n      a\n      \n    `;\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"${dirty}\" />`,\n          angularOutput: `<img class=\"${clean}\" />`,\n          html: `<img class=\"${dirty}\" />`,\n          htmlOutput: `<img class=\"${clean}\" />`,\n          jsx: `() => <img class={\\`${dirty}\\`} />`,\n          jsxOutput: `() => <img class={\\`${clean}\\`} />`,\n          svelte: `<img class={\\`${dirty}\\`} />`,\n          svelteOutput: `<img class={\\`${clean}\\`} />`,\n          vue: `<template><img class=\"${dirty}\" /></template>`,\n          vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes around expressions in template literals\", () => {\n\n    const dirtyExpression = \"${true ? 'true' : 'false'}\";\n    const cleanExpression = \"${true ? 'true' : 'false'}\";\n\n    const dirtyWithExpressions = dedent`\n      a\n      b\n      ${dirtyExpression}\n      a\n      c\n    `;\n    const cleanWithExpressions = dedent`\n      a\n      b\n      ${cleanExpression}\n      \n      c\n    `;\n\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyWithExpressions}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanWithExpressions}\\`} />`,\n          svelte: `<img class={\\`${dirtyWithExpressions}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanWithExpressions}\\`} />`,\n\n          errors: 1\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes around template literal elements\", () => {\n\n    const dirtyExpression = \"${true ? 'true' : 'false'}\";\n    const cleanExpression = \"${true ? 'true' : 'false'}\";\n\n    const dirtyExpressionAtStart = dedent`\n      a\n      b\n      a\n      ${dirtyExpression}\n    `;\n    const cleanExpressionAtStart = dedent`\n      a\n      b\n      \n      ${cleanExpression}\n    `;\n\n    const dirtyExpressionBetween = dedent`\n      a\n      b\n      a\n      ${dirtyExpression}\n      c\n      b\n      c\n    `;\n    const cleanExpressionBetween = dedent`\n      a\n      b\n      \n      ${cleanExpression}\n      c\n      \n      \n    `;\n\n    const dirtyExpressionAtEnd = dedent`\n      ${dirtyExpression}\n      a\n      b\n      a\n    `;\n    const cleanExpressionAtEnd = dedent`\n      ${cleanExpression}\n      a\n      b\n      \n    `;\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionAtStart}\\`} />`,\n\n          errors: 1\n        }\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionBetween}\\`} />`,\n\n          errors: 3\n        }\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionAtEnd}\\`} />`,\n\n          errors: 1\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes inside template literal elements\", () => {\n\n    const dirtyExpression = \"${true ? ' a b a c ' : ' b a b c '}\";\n    const cleanExpression = \"${true ? ' a b   ' : ' b a   '}\";\n\n    const dirtyStickyExpressionAtStart = `${dirtyExpression} c `;\n    const cleanStickyExpressionAtStart = `${cleanExpression} c `;\n\n    const dirtyStickyExpressionBetween = ` c ${dirtyExpression} d `;\n    const cleanStickyExpressionBetween = ` c ${cleanExpression} d `;\n\n    const dirtyStickyExpressionAtEnd = ` c ${dirtyExpression}`;\n    const cleanStickyExpressionAtEnd = ` c ${cleanExpression}`;\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n\n          errors: 4\n        }\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n\n          errors: 4\n        }\n\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n\n          errors: 4\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes inside nested template literal elements\", () => {\n\n    const dirtyExpression = \"${true ? ` a b ${false} a c ` : ` b a b c `}\";\n    const cleanExpression = \"${true ? ` a b ${false}   ` : ` b a   `}\";\n\n    const dirtyStickyExpressionAtStart = `${dirtyExpression} c `;\n    const cleanStickyExpressionAtStart = `${cleanExpression} c `;\n\n    const dirtyStickyExpressionBetween = ` c ${dirtyExpression} d `;\n    const cleanStickyExpressionBetween = ` c ${cleanExpression} d `;\n\n    const dirtyStickyExpressionAtEnd = ` c ${dirtyExpression}`;\n    const cleanStickyExpressionAtEnd = ` c ${cleanExpression}`;\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n\n          errors: 4\n        }\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n\n          errors: 4\n        }\n\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n\n          errors: 4\n        }\n      ]\n    });\n\n  });\n\n  it(\"should not remove sticky duplicate classes\", () => {\n\n    const dirtyExpression = \"${true ? 'true' : 'false'}\";\n    const cleanExpression = \"${true ? 'true' : 'false'}\";\n\n    const dirtyStickyExpressionAtStart = `${dirtyExpression}a b a b`;\n    const cleanStickyExpressionAtStart = `${cleanExpression}a b a `;\n\n    const dirtyStickyExpressionBetween = `a b a b${dirtyExpression}c d c d`;\n    const cleanStickyExpressionBetween = `a b  b${cleanExpression}c d c `;\n\n    const dirtyStickyExpressionAtEnd = `a b a b${dirtyExpression}`;\n    const cleanStickyExpressionAtEnd = `a b  b${cleanExpression}`;\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n\n          errors: 1\n        }\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n\n          errors: 2\n        }\n\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n\n          errors: 1\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes in defined call signature arguments\", () => {\n\n    const dirtyDefined = \"defined('  a b a  ');\";\n    const cleanDefined = \"defined('  a b   ');\";\n    const dirtyUndefined = \"notDefined(\\\"  a b a  \\\");\";\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: dirtyDefined,\n          jsxOutput: cleanDefined,\n          svelte: `<script>${dirtyDefined}</script>`,\n          svelteOutput: `<script>${cleanDefined}</script>`,\n          vue: `<script>${dirtyDefined}</script>`,\n          vueOutput: `<script>${cleanDefined}</script>`,\n\n          errors: 1,\n          options: [{ callees: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: dirtyUndefined,\n          svelte: `<script>${dirtyUndefined}</script>`,\n          vue: `<script>${dirtyUndefined}</script>`,\n\n          options: [{ callees: [\"defined\"] }]\n        }\n      ]\n    });\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: dirtyDefined,\n          jsxOutput: cleanDefined,\n          svelte: `<script>${dirtyDefined}</script>`,\n          svelteOutput: `<script>${cleanDefined}</script>`,\n          vue: `<script>${dirtyDefined}</script>`,\n          vueOutput: `<script>${cleanDefined}</script>`,\n\n          errors: 1,\n          options: [{ callees: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: dirtyUndefined,\n          svelte: `<script>${dirtyUndefined}</script>`,\n          vue: `<script>${dirtyUndefined}</script>`,\n\n          options: [{ callees: [\"defined\"] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes in string literals in defined variable declarations\", () => {\n\n    const dirtyDefined = \"const defined = \\\"  a b a  \\\";\";\n    const cleanDefined = \"const defined = \\\"  a b   \\\";\";\n    const dirtyUndefined = \"const notDefined = \\\"  a b a  \\\";\";\n\n    const dirtyMultiline = `const defined = \\`\n      a  b\n      a  c\n    \\`;`;\n\n    const cleanMultiline = `const defined = \\`\n      a  b\n        c\n    \\`;`;\n\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          jsx: dirtyDefined,\n          jsxOutput: cleanDefined,\n          svelte: `<script>${dirtyDefined}</script>`,\n          svelteOutput: `<script>${cleanDefined}</script>`,\n          vue: `<script>${dirtyDefined}</script>`,\n          vueOutput: `<script>${cleanDefined}</script>`,\n\n          errors: 1,\n          options: [{ variables: [\"defined\"] }]\n        },\n        {\n          jsx: dirtyMultiline,\n          jsxOutput: cleanMultiline,\n          svelte: `<script>${dirtyMultiline}</script>`,\n          svelteOutput: `<script>${cleanMultiline}</script>`,\n          vue: `<script>${dirtyMultiline}</script>`,\n          vueOutput: `<script>${cleanMultiline}</script>`,\n\n          errors: 1,\n          options: [{ variables: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: dirtyUndefined,\n          svelte: `<script>${dirtyUndefined}</script>`,\n          vue: `<script>${dirtyUndefined}</script>`\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove duplicate classes in string literals in defined tagged template literals\", () => {\n    lint(\n      noDuplicateClasses,\n\n      {\n        invalid: [\n          {\n            jsx: \"defined` a b a `\",\n            jsxOutput: \"defined` a b  `\",\n            svelte: \"<script>defined` a b a `</script>\",\n            svelteOutput: \"<script>defined` a b  `</script>\",\n            vue: \"defined` a b a `\",\n            vueOutput: \"defined` a b  `\",\n\n            errors: 1,\n            options: [{ tags: [\"defined\"] }]\n          }\n        ],\n        valid: [\n          {\n            jsx: \"notDefined` a b a `\",\n            svelte: \"<script>notDefined` a b a `</script>\",\n            vue: \"notDefined` a b a `\",\n\n            options: [{ tags: [\"defined\"] }]\n          }\n        ]\n      }\n    );\n  });\n\n  // #81\n  it(\"should not report duplicates for carriage return characters\", () => {\n    lint(noDuplicateClasses, {\n      valid: [\n        {\n          html: `<img class=\"  b  a \\r\\n c  \\r\\n  d  \" />`,\n          jsx: `() => <img class=\"  b  a \\r\\n c  \\r\\n  d  \" />`,\n          svelte: `<img class=\"  b  a \\r\\n c  \\r\\n  d  \" />`,\n          vue: `<template><img class=\"  b  a \\r\\n c  \\r\\n  d  \" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should not report duplicates for newline characters\", () => {\n    lint(noDuplicateClasses, {\n      valid: [\n        {\n          html: `<img class=\"  b  a \\n c  \\n  d  \" />`,\n          jsx: `() => <img class=\"  b  a \\n c  \\n  d  \" />`,\n          svelte: `<img class=\"  b  a \\n c  \\n  d  \" />`,\n          vue: `<template><img class=\"  b  a \\n c  \\n  d  \" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should report fixes with unchanged line endings\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img class=\"  b  a \\r\\n c  \\r\\n a d  \" />`,\n          htmlOutput: `<img class=\"  b  a \\r\\n c  \\r\\n  d  \" />`,\n          jsx: `() => <img class=\"  b  a \\r\\n c  \\r\\n a d  \" />`,\n          jsxOutput: `() => <img class=\"  b  a \\r\\n c  \\r\\n  d  \" />`,\n          svelte: `<img class=\"  b  a \\r\\n c  \\r\\n a d  \" />`,\n          svelteOutput: `<img class=\"  b  a \\r\\n c  \\r\\n  d  \" />`,\n          vue: `<template><img class=\"  b  a \\r\\n c  \\r\\n a d  \" /></template>`,\n          vueOutput: `<template><img class=\"  b  a \\r\\n c  \\r\\n  d  \" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img class=\"  b  a \\n c  \\n a d  \" />`,\n          htmlOutput: `<img class=\"  b  a \\n c  \\n  d  \" />`,\n          jsx: `() => <img class=\"  b  a \\n c  \\n a d  \" />`,\n          jsxOutput: `() => <img class=\"  b  a \\n c  \\n  d  \" />`,\n          svelte: `<img class=\"  b  a \\n c  \\n a d  \" />`,\n          svelteOutput: `<img class=\"  b  a \\n c  \\n  d  \" />`,\n          vue: `<template><img class=\"  b  a \\n c  \\n a d  \" /></template>`,\n          vueOutput: `<template><img class=\"  b  a \\n c  \\n  d  \" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n\n});\n"
  },
  {
    "path": "src/rules/no-duplicate-classes.ts",
    "content": "import { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { isClassSticky, splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const noDuplicateClasses = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Disallow duplicate class names in tailwind classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-duplicate-classes.md\",\n  name: \"no-duplicate-classes\",\n  recommended: true,\n\n  messages: {\n    duplicate: \"Duplicate classname: \\\"{{ className }}\\\".\"\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\n\nfunction lintLiterals(ctx: Context<typeof noDuplicateClasses>, literals: Literal[]) {\n  for(const literal of literals){\n\n    const parentClasses = literal.priorLiterals\n      ? getClassesFromLiteralNodes(literal.priorLiterals)\n      : [];\n\n    lintClasses(ctx, literal, (className, index, after) => {\n\n      const duplicateClassIndex = after.findIndex((afterClass, afterIndex) => afterClass === className && afterIndex < index);\n\n      // always keep sticky classes\n      if(isClassSticky(literal, index) || isClassSticky(literal, duplicateClassIndex)){\n        return;\n      }\n\n      if(parentClasses.includes(className) || duplicateClassIndex !== -1){\n        return {\n          data: { className },\n          fix: \"\",\n          id: \"duplicate\"\n        } as const;\n      }\n    });\n  }\n}\n\nfunction getClassesFromLiteralNodes(literals: Literal[]) {\n  return literals.reduce<string[]>((combinedClasses, literal) => {\n    if(!literal){ return combinedClasses; }\n\n    const classes = literal.content;\n    const split = splitClasses(classes);\n\n    for(const className of split){\n      if(!combinedClasses.includes(className)){\n        combinedClasses.push(className);\n      }\n    }\n\n    return combinedClasses;\n\n  }, []);\n}\n"
  },
  {
    "path": "src/rules/no-restricted-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noRestrictedClasses } from \"better-tailwindcss:rules/no-restricted-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\n\n\ndescribe(noRestrictedClasses.name, () => {\n\n  it(\"should not report on unrestricted classes\", () => {\n    lint(noRestrictedClasses, {\n      valid: [\n        {\n          angular: `<img class=\"font-bold container text-lg\" />`,\n          html: `<img class=\"font-bold container text-lg\" />`,\n          jsx: `() => <img class=\"font-bold container text-lg\" />`,\n          svelte: `<img class=\"font-bold container text-lg\" />`,\n          vue: `<template><img class=\"font-bold container text-lg\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should report restricted classes\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold container text-lg\" />`,\n          html: `<img class=\"font-bold container text-lg\" />`,\n          jsx: `() => <img class=\"font-bold container text-lg\" />`,\n          svelte: `<img class=\"font-bold container text-lg\" />`,\n          vue: `<template><img class=\"font-bold container text-lg\" /></template>`,\n\n          errors: 1,\n          options: [{ restrict: [\"container\"] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should report restricted classes matching a regex\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold container text-lg\" />`,\n          html: `<img class=\"font-bold container text-lg\" />`,\n          jsx: `() => <img class=\"font-bold container text-lg\" />`,\n          svelte: `<img class=\"font-bold container text-lg\" />`,\n          vue: `<template><img class=\"font-bold container text-lg\" /></template>`,\n\n          errors: 1,\n          options: [{ restrict: [\"^container$\"] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should report restricted classes with variants\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold lg:container lg:text-lg\" />`,\n          html: `<img class=\"font-bold lg:container lg:text-lg\" />`,\n          jsx: `() => <img class=\"font-bold lg:container lg:text-lg\" />`,\n          svelte: `<img class=\"font-bold lg:container lg:text-lg\" />`,\n          vue: `<template><img class=\"font-bold lg:container lg:text-lg\" /></template>`,\n\n          errors: 2,\n          options: [{ restrict: [\"^lg:.*\"] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should report restricted classes containing reserved regex characters\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold *:container **:text-lg\" />`,\n          html: `<img class=\"font-bold *:container **:text-lg\" />`,\n          jsx: `() => <img class=\"font-bold *:container **:text-lg\" />`,\n          svelte: `<img class=\"font-bold *:container **:text-lg\" />`,\n          vue: `<template><img class=\"font-bold *:container **:text-lg\" /></template>`,\n\n          errors: 2,\n          options: [{ restrict: [\"^\\\\*+:.*\"] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should be possible to disallow the important modifier\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold text-lg!\" />`,\n          html: `<img class=\"font-bold text-lg!\" />`,\n          jsx: `() => <img class=\"font-bold text-lg!\" />`,\n          svelte: `<img class=\"font-bold text-lg!\" />`,\n          vue: `<template><img class=\"font-bold text-lg!\" /></template>`,\n\n          errors: 1,\n          options: [{ restrict: [\"^.*!$\"] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should be possible to provide custom error messages\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold text-green-500 text-lg\" />`,\n          html: `<img class=\"font-bold text-green-500 text-lg\" />`,\n          jsx: `() => <img class=\"font-bold text-green-500 text-lg\" />`,\n          svelte: `<img class=\"font-bold text-green-500 text-lg\" />`,\n          vue: `<template><img class=\"font-bold text-green-500 text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use '*-success' instead.\" }\n          ],\n          options: [{ restrict: [{ message: \"Restricted class: Use '*-success' instead.\", pattern: \"^(.*)-green-(.*)$\" }] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should be possible to use matched groups in the error messages\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold text-green-500 text-lg\" />`,\n          html: `<img class=\"font-bold text-green-500 text-lg\" />`,\n          jsx: `() => <img class=\"font-bold text-green-500 text-lg\" />`,\n          svelte: `<img class=\"font-bold text-green-500 text-lg\" />`,\n          vue: `<template><img class=\"font-bold text-green-500 text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'text-success' instead.\" }\n          ],\n          options: [{ restrict: [{ message: \"Restricted class: Use '$1-success' instead.\", pattern: \"^(.*)-green-500$\" }] }]\n        },\n        {\n          angular: `<img class=\"font-bold bg-green-500 text-lg\" />`,\n          html: `<img class=\"font-bold bg-green-500 text-lg\" />`,\n          jsx: `() => <img class=\"font-bold bg-green-500 text-lg\" />`,\n          svelte: `<img class=\"font-bold bg-green-500 text-lg\" />`,\n          vue: `<template><img class=\"font-bold bg-green-500 text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'bg-success' instead.\" }\n          ],\n          options: [{ restrict: [{ message: \"Restricted class: Use '$1-success' instead.\", pattern: \"^(.*)-green-500$\" }] }]\n        }\n      ]\n    });\n  });\n\n  it(\"should fix the classes when a fix is provided\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold bg-green-500 text-green-500 text-lg\" />`,\n          angularOutput: `<img class=\"font-bold bg-success text-success text-lg\" />`,\n          html: `<img class=\"font-bold bg-green-500 text-green-500 text-lg\" />`,\n          htmlOutput: `<img class=\"font-bold bg-success text-success text-lg\" />`,\n          jsx: `() => <img class=\"font-bold bg-green-500 text-green-500 text-lg\" />`,\n          jsxOutput: `() => <img class=\"font-bold bg-success text-success text-lg\" />`,\n          svelte: `<img class=\"font-bold bg-green-500 text-green-500 text-lg\" />`,\n          svelteOutput: `<img class=\"font-bold bg-success text-success text-lg\" />`,\n          vue: `<template><img class=\"font-bold bg-green-500 text-green-500 text-lg\" /></template>`,\n          vueOutput: `<template><img class=\"font-bold bg-success text-success text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'bg-success' instead.\" },\n            { message: \"Restricted class: Use 'text-success' instead.\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"$1-success\",\n              message: \"Restricted class: Use '$1-success' instead.\",\n              pattern: \"^(text|bg)-green-500$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should fix only the class name when a variant is used\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold lg:text-green-500 text-lg\" />`,\n          angularOutput: `<img class=\"font-bold lg:text-success text-lg\" />`,\n          html: `<img class=\"font-bold lg:text-green-500 text-lg\" />`,\n          htmlOutput: `<img class=\"font-bold lg:text-success text-lg\" />`,\n          jsx: `() => <img class=\"font-bold lg:text-green-500 text-lg\" />`,\n          jsxOutput: `() => <img class=\"font-bold lg:text-success text-lg\" />`,\n          svelte: `<img class=\"font-bold lg:text-green-500 text-lg\" />`,\n          svelteOutput: `<img class=\"font-bold lg:text-success text-lg\" />`,\n          vue: `<template><img class=\"font-bold lg:text-green-500 text-lg\" /></template>`,\n          vueOutput: `<template><img class=\"font-bold lg:text-success text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'lg:text-success' instead.\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"$1$2-success\",\n              message: \"Restricted class: Use '$1$2-success' instead.\",\n              pattern: \"^([a-zA-Z0-9:/_-]*:)?(text|bg)-green-500$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should fix classes with multiple variants\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold lg:hover:text-green-500 text-lg\" />`,\n          angularOutput: `<img class=\"font-bold lg:hover:text-success text-lg\" />`,\n          html: `<img class=\"font-bold lg:hover:text-green-500 text-lg\" />`,\n          htmlOutput: `<img class=\"font-bold lg:hover:text-success text-lg\" />`,\n          jsx: `() => <img class=\"font-bold lg:hover:text-green-500 text-lg\" />`,\n          jsxOutput: `() => <img class=\"font-bold lg:hover:text-success text-lg\" />`,\n          svelte: `<img class=\"font-bold lg:hover:text-green-500 text-lg\" />`,\n          svelteOutput: `<img class=\"font-bold lg:hover:text-success text-lg\" />`,\n          vue: `<template><img class=\"font-bold lg:hover:text-green-500 text-lg\" /></template>`,\n          vueOutput: `<template><img class=\"font-bold lg:hover:text-success text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'lg:hover:text-success' instead.\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"$1$2-success\",\n              message: \"Restricted class: Use '$1$2-success' instead.\",\n              pattern: \"^([a-zA-Z0-9:/_-]*:)?(text|bg)-green-500$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should match modifiers\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold lg:hover:text-green-500/50 text-lg\" />`,\n          angularOutput: `<img class=\"font-bold lg:hover:text-success/50 text-lg\" />`,\n          html: `<img class=\"font-bold lg:hover:text-green-500/50 text-lg\" />`,\n          htmlOutput: `<img class=\"font-bold lg:hover:text-success/50 text-lg\" />`,\n          jsx: `() => <img class=\"font-bold lg:hover:text-green-500/50 text-lg\" />`,\n          jsxOutput: `() => <img class=\"font-bold lg:hover:text-success/50 text-lg\" />`,\n          svelte: `<img class=\"font-bold lg:hover:text-green-500/50 text-lg\" />`,\n          svelteOutput: `<img class=\"font-bold lg:hover:text-success/50 text-lg\" />`,\n          vue: `<template><img class=\"font-bold lg:hover:text-green-500/50 text-lg\" /></template>`,\n          vueOutput: `<template><img class=\"font-bold lg:hover:text-success/50 text-lg\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'lg:hover:text-success/50' instead.\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"$1$2-success$3\",\n              message: \"Restricted class: Use '$1$2-success$3' instead.\",\n              pattern: \"^([a-zA-Z0-9:/_-]*:)?(text|bg)-green-500(\\\\/[0-9]{1,3})?$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should work on multiline literals\", () => {\n    const dirty = dedent`\n      bg-green-500\n      hover:text-green-500\n    `;\n\n    const clean = dedent`\n      bg-success\n      hover:text-success\n    `;\n\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"${dirty}\" />`,\n          angularOutput: `<img class=\"${clean}\" />`,\n          html: `<img class=\"${dirty}\" />`,\n          htmlOutput: `<img class=\"${clean}\" />`,\n          jsx: `() => <img class=\"${dirty}\" />`,\n          jsxOutput: `() => <img class=\"${clean}\" />`,\n          svelte: `<img class=\"${dirty}\" />`,\n          svelteOutput: `<img class=\"${clean}\" />`,\n          vue: `<template><img class=\"${dirty}\" /></template>`,\n          vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'bg-success' instead.\" },\n            { message: \"Restricted class: Use 'hover:text-success' instead.\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"$1$2-success\",\n              message: \"Restricted class: Use '$1$2-success' instead.\",\n              pattern: \"^([a-zA-Z0-9:/_-]*:)?(text|bg)-green-500$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not report on classes with the same name but different variants\", () => {\n    lint(noRestrictedClasses, {\n      valid: [\n        {\n          angular: `<img class=\"font-bold text-green-500\" />`,\n          html: `<img class=\"font-bold text-green-500\" />`,\n          jsx: `() => <img class=\"font-bold text-green-500\" />`,\n          svelte: `<img class=\"font-bold text-green-500\" />`,\n          vue: `<template><img class=\"font-bold text-green-500\" /></template>`,\n\n          options: [{\n            restrict: [{\n              message: \"Restricted class: Use 'hover:text-success' instead.\",\n              pattern: \"^hover:text-green-500$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should be possible to remove classes with a fix\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"font-bold text-green-500\" />`,\n          angularOutput: `<img class=\"font-bold \" />`,\n          html: `<img class=\"font-bold text-green-500\" />`,\n          htmlOutput: `<img class=\"font-bold \" />`,\n          jsx: `() => <img class=\"font-bold text-green-500\" />`,\n          jsxOutput: `() => <img class=\"font-bold \" />`,\n          svelte: `<img class=\"font-bold text-green-500\" />`,\n          svelteOutput: `<img class=\"font-bold \" />`,\n          vue: `<template><img class=\"font-bold text-green-500\" /></template>`,\n          vueOutput: `<template><img class=\"font-bold \" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: text-green-500 is not allowed.\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"\",\n              message: \"Restricted class: text-green-500 is not allowed.\",\n              pattern: \"^text-green-500$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should work with mixed string and object restrictions\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"text-green-500 bg-red-500\" />`,\n          angularOutput: `<img class=\"text-success bg-red-500\" />`,\n          html: `<img class=\"text-green-500 bg-red-500\" />`,\n          htmlOutput: `<img class=\"text-success bg-red-500\" />`,\n          jsx: `() => <img class=\"text-green-500 bg-red-500\" />`,\n          jsxOutput: `() => <img class=\"text-success bg-red-500\" />`,\n          svelte: `<img class=\"text-green-500 bg-red-500\" />`,\n          svelteOutput: `<img class=\"text-success bg-red-500\" />`,\n          vue: `<template><img class=\"text-green-500 bg-red-500\" /></template>`,\n          vueOutput: `<template><img class=\"text-success bg-red-500\" /></template>`,\n\n          errors: [\n            { message: \"Custom message for green\" },\n            { message: \"Restricted class: \\\"bg-red-500\\\".\" }\n          ],\n          options: [{\n            restrict: [\n              { fix: \"text-success\", message: \"Custom message for green\", pattern: \"^text-green-500$\" },\n              \"^bg-red-500$\"\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should fallback to empty string for invalid capture groups in messages\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"text-green-500\" />`,\n          html: `<img class=\"text-green-500\" />`,\n          jsx: `() => <img class=\"text-green-500\" />`,\n          svelte: `<img class=\"text-green-500\" />`,\n          vue: `<template><img class=\"text-green-500\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use '' instead of 'text-green-500'.\" }\n          ],\n          options: [{\n            restrict: [{\n              message: \"Restricted class: Use '$10' instead of '$1'.\",\n              pattern: \"^(text-green-500)$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should fallback to empty string for invalid capture groups in fixes\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"text-green-500\" />`,\n          angularOutput: `<img class=\"\" />`,\n          html: `<img class=\"text-green-500\" />`,\n          htmlOutput: `<img class=\"\" />`,\n          jsx: `() => <img class=\"text-green-500\" />`,\n          jsxOutput: `() => <img class=\"\" />`,\n          svelte: `<img class=\"text-green-500\" />`,\n          svelteOutput: `<img class=\"\" />`,\n          vue: `<template><img class=\"text-green-500\" /></template>`,\n          vueOutput: `<template><img class=\"\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: \\\"text-green-500\\\".\" }\n          ],\n          options: [{\n            restrict: [{\n              fix: \"$10\",\n              pattern: \"^(text-green-500)$\"\n            }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should report the first matching restrictions\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"text-green-500\" />`,\n          html: `<img class=\"text-green-500\" />`,\n          jsx: `() => <img class=\"text-green-500\" />`,\n          svelte: `<img class=\"text-green-500\" />`,\n          vue: `<template><img class=\"text-green-500\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: Use 'text-success' instead.\" }\n          ],\n          options: [{\n            restrict: [\n              { message: \"Restricted class: Use 'text-success' instead.\", pattern: \"^text-green-500$\" },\n              { message: \"Match any green color class\", pattern: \".*green.*\" },\n              \"^text-green-500$\"\n            ]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should work with restriction objects without fix and message\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"text-green-500\" />`,\n          html: `<img class=\"text-green-500\" />`,\n          jsx: `() => <img class=\"text-green-500\" />`,\n          svelte: `<img class=\"text-green-500\" />`,\n          vue: `<template><img class=\"text-green-500\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: \\\"text-green-500\\\".\" }\n          ],\n          options: [{\n            restrict: [{ pattern: \"^text-green-500$\" }]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should fallback to the default message when empty\", () => {\n    lint(noRestrictedClasses, {\n      invalid: [\n        {\n          angular: `<img class=\"text-green-500\" />`,\n          html: `<img class=\"text-green-500\" />`,\n          jsx: `() => <img class=\"text-green-500\" />`,\n          svelte: `<img class=\"text-green-500\" />`,\n          vue: `<template><img class=\"text-green-500\" /></template>`,\n\n          errors: [\n            { message: \"Restricted class: \\\"text-green-500\\\".\" }\n          ],\n          options: [{\n            restrict: [{ message: \"\", pattern: \"^text-green-500$\" }]\n          }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/rules/no-restricted-classes.ts",
    "content": "import {\n  array,\n  description,\n  optional,\n  pipe,\n  strictObject,\n  string,\n  union\n} from \"valibot\";\n\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { replacePlaceholders } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const noRestrictedClasses = createRule({\n  autofix: true,\n  category: \"correctness\",\n  description: \"Disallow restricted classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-restricted-classes.md\",\n  name: \"no-restricted-classes\",\n  recommended: false,\n\n  schema: strictObject({\n    restrict: optional(\n      array(\n        union([\n          strictObject(\n            {\n              fix: optional(\n                pipe(\n                  string(),\n                  description(\"A replacement class\")\n                )\n              ),\n              message: optional(\n                pipe(\n                  string(),\n                  description(\"The message to report when a class is restricted.\")\n                )\n              ),\n              pattern: pipe(\n                string(),\n                description(\"The regex pattern to match restricted classes.\")\n              )\n            }\n          ),\n          string()\n        ])\n      ),\n      []\n    )\n  }),\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\n\nfunction lintLiterals(ctx: Context<typeof noRestrictedClasses>, literals: Literal[]) {\n\n  const { restrict: restrictions } = ctx.options;\n\n  for(const literal of literals){\n    lintClasses(ctx, literal, (className, classes) => {\n\n      for(const restriction of restrictions){\n        const pattern = typeof restriction === \"string\"\n          ? restriction\n          : restriction.pattern;\n\n        const matches = className.match(pattern);\n\n        if(!matches){\n          continue;\n        }\n\n        const message = typeof restriction === \"string\" || !restriction.message\n          ? `Restricted class: \"${className}\".`\n          : replacePlaceholders(restriction.message, matches);\n\n        if(typeof restriction === \"string\"){\n          return {\n            message\n          } as const;\n        }\n\n        if(restriction.fix !== undefined){\n          return {\n            fix: replacePlaceholders(restriction.fix, matches),\n            message\n          } as const;\n        }\n\n        return {\n          message\n        } as const;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "src/rules/no-unknown-classes.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noUnknownClasses } from \"better-tailwindcss:rules/no-unknown-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { css, ts } from \"better-tailwindcss:tests/utils/template.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\n\n\ndescribe(noUnknownClasses.name, () => {\n\n  it(\"should not report standard tailwind classes\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"flex\" />`,\n            html: `<img class=\"flex\" />`,\n            jsx: `() => <img class=\"flex\" />`,\n            svelte: `<img class=\"flex\" />`,\n            vue: `<template><img class=\"flex\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report standard tailwind classes with variants\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"hover:flex\" />`,\n            html: `<img class=\"hover:flex\" />`,\n            jsx: `() => <img class=\"hover:flex\" />`,\n            svelte: `<img class=\"hover:flex\" />`,\n            vue: `<template><img class=\"hover:flex\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report standard tailwind classes with many variants\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"dark:hover:before:inset-0\" />`,\n            html: `<img class=\"dark:hover:before:inset-0\" />`,\n            jsx: `() => <img class=\"dark:hover:before:inset-0\" />`,\n            svelte: `<img class=\"dark:hover:before:inset-0\" />`,\n            vue: `<template><img class=\"dark:hover:before:inset-0\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should report standard tailwind classes with an unknown variant in many variants\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"dark:unknown:before:inset-0\" />`,\n            html: `<img class=\"dark:unknown:before:inset-0\" />`,\n            jsx: `() => <img class=\"dark:unknown:before:inset-0\" />`,\n            svelte: `<img class=\"dark:unknown:before:inset-0\" />`,\n            vue: `<template><img class=\"dark:unknown:before:inset-0\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not report on dynamic utility values in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"py-2.25\" />`,\n            html: `<img class=\"py-2.25\" />`,\n            jsx: `() => <img class=\"py-2.25\" />`,\n            svelte: `<img class=\"py-2.25\" />`,\n            vue: `<template><img class=\"py-2.25\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should report on dynamic utility values in tailwind <= 3\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"py-2.25\" />`,\n            html: `<img class=\"py-2.25\" />`,\n            jsx: `() => <img class=\"py-2.25\" />`,\n            svelte: `<img class=\"py-2.25\" />`,\n            vue: `<template><img class=\"py-2.25\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should report unknown classes\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"unknown\" />`,\n            html: `<img class=\"unknown\" />`,\n            jsx: `() => <img class=\"unknown\" />`,\n            svelte: `<img class=\"unknown\" />`,\n            vue: `<template><img class=\"unknown\" /></template>`,\n\n            errors: 1\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should be possible to whitelist classes in options\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"unknown\" />`,\n            html: `<img class=\"unknown\" />`,\n            jsx: `() => <img class=\"unknown\" />`,\n            svelte: `<img class=\"unknown\" />`,\n            vue: `<template><img class=\"unknown\" /></template>`,\n\n            options: [{ ignore: [\"unknown\"] }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should be possible to whitelist classes in options via regex\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"ignored-unknown\" />`,\n            html: `<img class=\"ignored-unknown\" />`,\n            jsx: `() => <img class=\"ignored-unknown\" />`,\n            svelte: `<img class=\"ignored-unknown\" />`,\n            vue: `<template><img class=\"ignored-unknown\" /></template>`,\n\n            options: [{ ignore: [\"^ignored-.*$\"] }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not report on registered utility classes in tailwind <= 3\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            html: `<img class=\"unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            jsx: `() => <img class=\"unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            svelte: `<img class=\"unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            vue: `<template><img class=\"unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"plugin.js\": ts`\n                export function plugin() {\n                  return function({ addUtilities }) {\n                    addUtilities({\n                      \".in-plugin\": {\n                        color: \"red\"\n                      }\n                    });\n                  };\n                }\n              `,\n              \"tailwind.config.color.js\": ts`\n                import { plugin } from \"./plugin.js\";\n\n                export default {\n                  plugins: [\n                    plugin()\n                  ],\n                  theme: {\n                    extend: {\n                      colors: {\n                        config: \"red\"\n                      }\n                    }\n                  }\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.color.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not report on registered utility classes in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"in-utility unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            html: `<img class=\"in-utility unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            jsx: `() => <img class=\"in-utility unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            svelte: `<img class=\"in-utility unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" />`,\n            vue: `<template><img class=\"in-utility unknown in-plugin text-config hover:before:in-plugin hover:before:text-config\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"plugin.js\": ts`\n                import createPlugin from \"tailwindcss/plugin\";\n\n                export default createPlugin(({ addUtilities }) => {\n                  addUtilities({\n                    \".in-plugin\": {\n                      color: \"red\"\n                    }\n                  });\n                });\n              `,\n              \"tailwind.config.js\": ts`\n                export default {\n                  theme: {\n                    extend: {\n                      colors: {\n                        config: \"red\"\n                      }\n                    }\n                  }\n                };\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                @config \"./tailwind.config.js\";\n                @plugin \"./plugin.js\";\n\n                @utility in-utility {\n                  @apply text-red-500;\n                }\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should ignore custom component classes defined in the component layer in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                @layer components {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ],\n        valid: [\n          {\n            angular: `<img class=\"custom-component\" />`,\n            html: `<img class=\"custom-component\" />`,\n            jsx: `() => <img class=\"custom-component\" />`,\n            svelte: `<img class=\"custom-component\" />`,\n            vue: `<template><img class=\"custom-component\" /></template>`,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                @layer components {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should ignore custom component classes defined in imported files in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"components.css\": css`\n                @layer components {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./components.css\";\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"nested/dir/components.css\": css`\n                @layer components {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `,\n              \"nested/import.css\": css`\n                @import \"./dir/components.css\";\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./nested/import.css\";\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should ignore classes defined in imported files with layer(components) in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        // immediate layer import\n        invalid: [\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"components.css\": css`\n                .custom-component {\n                  font-weight: bold;\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"./components.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            // layer import via nested file\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"nested/dir/components.css\": css`\n                .custom-component {\n                  @apply font-bold;\n                }\n              `,\n              \"nested/import.css\": css`\n                @import \"./dir/components.css\";\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./nested/import.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            // layer import in nested file\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"nested/dir/components.css\": css`\n                .custom-component {\n                  @apply font-bold;\n                }\n              `,\n              \"nested/import.css\": css`\n                @import \"./dir/components.css\" layer(components);\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./nested/import.css\";\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should ignore classes defined in imported files in nested components.custom layer in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"nested/dir/components.css\": css`\n                .custom-component {\n                  @apply font-bold;\n                }\n              `,\n              \"nested/import.css\": css`\n                @import \"./dir/components.css\" layer(custom);\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./nested/import.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"nested/dir/components.css\": css`\n                @layer custom {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `,\n              \"nested/import.css\": css`\n                @import \"./dir/components.css\";\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./nested/import.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"nested/dir/components.css\": css`\n                @layer components {\n                  @layer custom {\n                    .custom-component {\n                      @apply font-bold;\n                    }\n                  }\n                }\n              `,\n              \"nested/import.css\": css`\n                @import \"./dir/components.css\";\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./nested/import.css\";\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not ignore custom classes from other layers in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 2,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                @layer custom {\n                  .custom-component {\n                    font-weight: bold;\n                  }\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 2,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                @layer custom {\n                  @layer components {\n                    .custom-component {\n                      font-weight: bold;\n                    }\n                  }\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 2,\n\n            files: {\n              \"./components.css\": css`\n                @layer components {\n                  .custom-component {\n                    font-weight: bold;\n                  }\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./components.css\" layer(custom);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 2,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                .custom-component {\n                  font-weight: bold;\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not crash when trying to read custom component classes in a file that doesn't exists in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"custom-component unknown\" />`,\n            html: `<img class=\"custom-component unknown\" />`,\n            jsx: `() => <img class=\"custom-component unknown\" />`,\n            svelte: `<img class=\"custom-component unknown\" />`,\n            vue: `<template><img class=\"custom-component unknown\" /></template>`,\n\n            errors: 2,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./does-not-exist.css\";\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should support variants in custom component classes in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        // immediate layer import\n        invalid: [\n          {\n            angular: `<img class=\"sm:hover:custom-component sm:hover:unknown\" />`,\n            html: `<img class=\"sm:hover:custom-component sm:hover:unknown\" />`,\n            jsx: `() => <img class=\"sm:hover:custom-component sm:hover:unknown\" />`,\n            svelte: `<img class=\"sm:hover:custom-component sm:hover:unknown\" />`,\n            vue: `<template><img class=\"sm:hover:custom-component sm:hover:unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"components.css\": css`\n                .custom-component {\n                  font-weight: bold;\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./components.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"[&[open]]:custom-component [&[open]]:unknown\" />`,\n            html: `<img class=\"[&[open]]:custom-component [&[open]]:unknown\" />`,\n            jsx: `() => <img class=\"[&[open]]:custom-component [&[open]]:unknown\" />`,\n            svelte: `<img class=\"[&[open]]:custom-component [&[open]]:unknown\" />`,\n            vue: `<template><img class=\"[&[open]]:custom-component [&[open]]:unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"components.css\": css`\n                .custom-component {\n                  font-weight: bold;\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n                @import \"./components.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should support prefixes in custom component classes in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        // immediate layer import\n        invalid: [\n          {\n            angular: `<img class=\"tw:md:custom-component tw:md:unknown\" />`,\n            html: `<img class=\"tw:md:custom-component tw:md:unknown\" />`,\n            jsx: `() => <img class=\"tw:md:custom-component tw:md:unknown\" />`,\n            svelte: `<img class=\"tw:md:custom-component tw:md:unknown\" />`,\n            vue: `<template><img class=\"tw:md:custom-component tw:md:unknown\" /></template>`,\n\n            errors: 1,\n\n            files: {\n              \"components.css\": css`\n                .custom-component {\n                  font-weight: bold;\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n                @import \"./components.css\" layer(components);\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should work with prefixed tailwind classes tailwind <= 3\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex tw-flex hover:tw-flex\"/>`,\n            html: `<img class=\"flex tw-flex hover:tw-flex\" />`,\n            jsx: `() => <img class=\"flex tw-flex hover:tw-flex\" />`,\n            svelte: `<img class=\"flex tw-flex hover:tw-flex\" />`,\n            vue: `<template><img class=\"flex tw-flex hover:tw-flex\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.config.prefix.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.prefix.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should work with prefixed tailwind classes tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"flex tw:flex tw:hover:flex\"/>`,\n            html: `<img class=\"flex tw:flex tw:hover:flex\" />`,\n            jsx: `() => <img class=\"flex tw:flex tw:hover:flex\" />`,\n            svelte: `<img class=\"flex tw:flex tw:hover:flex\" />`,\n            vue: `<template><img class=\"flex tw:flex tw:hover:flex\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not report on DaisyUI classes in tailwind <= 3\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<details class=\"dropdown-hover btn\"></details>`,\n            html: `<details class=\"dropdown-hover btn\"></details>`,\n            jsx: `() => <details class=\"dropdown-hover btn\"></details>`,\n            svelte: `<details class=\"dropdown-hover btn\"></details>`,\n            vue: `<template><details class=\"dropdown-hover btn\"></details></template>`,\n\n            files: {\n              \"tailwind.config.ts\": ts`\n                import daisyui from \"daisyui\";\n\n                export default {\n                  plugins: [\n                    daisyui\n                  ],\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.ts\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not report on DaisyUI classes in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<details class=\"dropdown-hover btn\"></details>`,\n            html: `<details class=\"dropdown-hover btn\"></details>`,\n            jsx: `() => <details class=\"dropdown-hover btn\"></details>`,\n            svelte: `<details class=\"dropdown-hover btn\"></details>`,\n            vue: `<template><details class=\"dropdown-hover btn\"></details></template>`,\n\n            errors: 1,\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\";\n\n                @plugin \"daisyui\";\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report on groups and peers\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"group\" />`,\n            html: `<img class=\"group\" />`,\n            jsx: `() => <img class=\"group\" />`,\n            svelte: `<img class=\"group\" />`,\n            vue: `<template><img class=\"group\" /></template>`\n          },\n          {\n            angular: `<img class=\"peer\" />`,\n            html: `<img class=\"peer\" />`,\n            jsx: `() => <img class=\"peer\" />`,\n            svelte: `<img class=\"peer\" />`,\n            vue: `<template><img class=\"peer\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report on named groups and peers\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"group/custom-group\" />`,\n            html: `<img class=\"group/custom-group\" />`,\n            jsx: `() => <img class=\"group/custom-group\" />`,\n            svelte: `<img class=\"group/custom-group\" />`,\n            vue: `<template><img class=\"group/custom-group\" /></template>`\n          },\n          {\n            angular: `<img class=\"peer/custom-peer\" />`,\n            html: `<img class=\"peer/custom-peer\" />`,\n            jsx: `() => <img class=\"peer/custom-peer\" />`,\n            svelte: `<img class=\"peer/custom-peer\" />`,\n            vue: `<template><img class=\"peer/custom-peer\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not report on prefixed groups and peers in tailwind <= 3\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"tw-group\"/>`,\n            html: `<img class=\"tw-group\" />`,\n            jsx: `() => <img class=\"tw-group\" />`,\n            svelte: `<img class=\"tw-group\" />`,\n            vue: `<template><img class=\"tw-group\" /></template>`,\n\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw-peer\"/>`,\n            html: `<img class=\"tw-peer\" />`,\n            jsx: `() => <img class=\"tw-peer\" />`,\n            svelte: `<img class=\"tw-peer\" />`,\n            vue: `<template><img class=\"tw-peer\" /></template>`,\n\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major <= 3)(\"should not report on prefixed named groups and peers in tailwind <= 3\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"tw-group/custom-group\"/>`,\n            html: `<img class=\"tw-group/custom-group\" />`,\n            jsx: `() => <img class=\"tw-group/custom-group\" />`,\n            svelte: `<img class=\"tw-group/custom-group\" />`,\n            vue: `<template><img class=\"tw-group/custom-group\" /></template>`,\n\n            files: {\n              \"tailwind.config.prefix.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.prefix.js\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw-peer/custom-peer\"/>`,\n            html: `<img class=\"tw-peer/custom-peer\" />`,\n            jsx: `() => <img class=\"tw-peer/custom-peer\" />`,\n            svelte: `<img class=\"tw-peer/custom-peer\" />`,\n            vue: `<template><img class=\"tw-peer/custom-peer\" /></template>`,\n\n            files: {\n              \"tailwind.config.js\": ts`\n                export default {\n                  prefix: 'tw-',\n                };\n              `\n            },\n            options: [{\n              tailwindConfig: \"./tailwind.config.js\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not report on prefixed groups and peers in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"tw:group\"/>`,\n            html: `<img class=\"tw:group\" />`,\n            jsx: `() => <img class=\"tw:group\" />`,\n            svelte: `<img class=\"tw:group\" />`,\n            vue: `<template><img class=\"tw:group\" /></template>`,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw:peer\"/>`,\n            html: `<img class=\"tw:peer\" />`,\n            jsx: `() => <img class=\"tw:peer\" />`,\n            svelte: `<img class=\"tw:peer\" />`,\n            vue: `<template><img class=\"tw:peer\" /></template>`,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should not report on prefixed named groups and peers in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"tw:group/custom-group\"/>`,\n            html: `<img class=\"tw:group/custom-group\" />`,\n            jsx: `() => <img class=\"tw:group/custom-group\" />`,\n            svelte: `<img class=\"tw:group/custom-group\" />`,\n            vue: `<template><img class=\"tw:group/custom-group\" /></template>`,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          },\n          {\n            angular: `<img class=\"tw:peer/custom-peer\"/>`,\n            html: `<img class=\"tw:peer/custom-peer\" />`,\n            jsx: `() => <img class=\"tw:peer/custom-peer\" />`,\n            svelte: `<img class=\"tw:peer/custom-peer\" />`,\n            vue: `<template><img class=\"tw:peer/custom-peer\" /></template>`,\n\n            files: {\n              \"tailwind.css\": css`\n                @import \"tailwindcss\" prefix(tw);\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it(\"should not report on tailwind utility classes with modifiers\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        valid: [\n          {\n            angular: `<img class=\"bg-red-500/50\" />`,\n            html: `<img class=\"bg-red-500/50\" />`,\n            jsx: `() => <img class=\"bg-red-500/50\" />`,\n            svelte: `<img class=\"bg-red-500/50\" />`,\n            vue: `<template><img class=\"bg-red-500/50\" /></template>`\n          },\n          {\n            angular: `<img class=\"hover:bg-red-500/50\" />`,\n            html: `<img class=\"hover:bg-red-500/50\" />`,\n            jsx: `() => <img class=\"hover:bg-red-500/50\" />`,\n            svelte: `<img class=\"hover:bg-red-500/50\" />`,\n            vue: `<template><img class=\"hover:bg-red-500/50\" /></template>`\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should support tsconfig paths in tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"unknown custom-component custom-utility custom-plugin\"/>`,\n            html: `<img class=\"unknown custom-component custom-utility custom-plugin\" />`,\n            jsx: `() => <img class=\"unknown custom-component custom-utility custom-plugin\" />`,\n            svelte: `<img class=\"unknown custom-component custom-utility custom-plugin\" />`,\n            vue: `<template><img class=\"unknown custom-component custom-utility custom-plugin\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"nested/components/custom-components.css\": css`\n                @layer components {\n                  .custom-component {\n                    @apply font-bold;\n                  }\n                }\n              `,\n              \"nested/plugins/custom-plugin.js\": ts`\n                import createPlugin from \"tailwindcss/plugin\";\n\n                export default createPlugin(({ addUtilities }) => {\n                  addUtilities({\n                    \".custom-plugin\": {\n                      fontWeight: \"bold\"\n                    }\n                  });\n                });\n              `,\n              \"nested/utilities/custom-utilities.css\": css`\n                @utility custom-utility {\n                  font-weight: bold;\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\"; \n                @import \"@components/custom-components.css\";\n                @import \"@utilities/custom-utilities.css\";\n                @plugin \"@plugins/custom-plugin.js\";\n              `,\n              \"tsconfig.json\": ts`\n                {\n                  \"compilerOptions\": {\n                    \"paths\": {\n                      \"@components/*\": [\"./nested/components/*\"],\n                      \"@utilities/*\": [\"./nested/utilities/*\"],\n                      \"@plugins/*\": [\"./nested/plugins/*\"]\n                    }\n                  }\n                }\n              `\n            },\n            options: [{\n              detectComponentClasses: true,\n              entryPoint: \"./tailwind.css\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n\n  it.runIf(getTailwindCSSVersion().major >= 4)(\"should use the provided tsconfig instead of finding one tailwind >= 4\", () => {\n    lint(\n      noUnknownClasses,\n      {\n        invalid: [\n          {\n            angular: `<img class=\"unknown custom-utility\"/>`,\n            html: `<img class=\"unknown custom-utility\" />`,\n            jsx: `() => <img class=\"unknown custom-utility\" />`,\n            svelte: `<img class=\"unknown custom-utility\" />`,\n            vue: `<template><img class=\"unknown custom-utility\" /></template>`,\n\n            errors: 1,\n            files: {\n              \"correct/custom-utilities.css\": css`\n                @utility custom-utility {\n                  font-weight: bold;\n                }\n              `,\n              \"tailwind.css\": css`\n                @import \"tailwindcss\"; \n                @import \"@correct/custom-utilities.css\";\n              `,\n              \"tsconfig-custom.json\": ts`\n                {\n                  \"compilerOptions\": {\n                    \"paths\": {\n                      \"@correct/*\": [\"./correct/*\"],\n                    }\n                  }\n                }\n              `,\n              \"tsconfig.json\": ts`\n                {\n                  \"compilerOptions\": {\n                    \"paths\": {\n                      \"@unused/*\": [\"./unused/*\"]\n                    }\n                  }\n                }\n              `\n            },\n            options: [{\n              entryPoint: \"./tailwind.css\",\n              tsconfig: \"./tsconfig-custom.json\"\n            }]\n          }\n        ]\n      }\n    );\n  });\n});\n"
  },
  {
    "path": "src/rules/no-unknown-classes.ts",
    "content": "import { array, description, optional, pipe, strictObject, string } from \"valibot\";\n\nimport {\n  createGetCustomComponentClasses,\n  getCustomComponentClasses\n} from \"better-tailwindcss:tailwindcss/custom-component-classes.js\";\nimport { createGetPrefix, getPrefix } from \"better-tailwindcss:tailwindcss/prefix.js\";\nimport { createGetUnknownClasses, getUnknownClasses } from \"better-tailwindcss:tailwindcss/unknown-classes.js\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\nimport { escapeForRegex } from \"better-tailwindcss:utils/escape.js\";\nimport { lintClasses } from \"better-tailwindcss:utils/lint.js\";\nimport { getCachedRegex } from \"better-tailwindcss:utils/regex.js\";\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { splitClasses } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const noUnknownClasses = createRule({\n  autofix: true,\n  category: \"correctness\",\n  description: \"Disallow any css classes that are not registered in tailwindcss.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-unknown-classes.md\",\n  name: \"no-unknown-classes\",\n  recommended: true,\n\n  messages: {\n    unknown: \"Unknown class detected: {{ className }}\"\n  },\n\n  schema: strictObject({\n    ignore: optional(\n      pipe(\n        array(\n          string()\n        ),\n        description(\"A list of regular expression patterns for classes that should be ignored by the rule.\")\n      ),\n      []\n    )\n  }),\n\n  initialize: ctx => {\n    const { detectComponentClasses } = ctx.options;\n\n    createGetPrefix(ctx);\n    createGetUnknownClasses(ctx);\n\n    if(detectComponentClasses){\n      createGetCustomComponentClasses(ctx);\n    }\n  },\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\n\nfunction lintLiterals(ctx: Context<typeof noUnknownClasses>, literals: Literal[]) {\n\n  const { ignore } = ctx.options;\n\n  const { prefix, suffix } = getPrefix(async(ctx));\n\n  const ignoredGroups = getCachedRegex(`^${escapeForRegex(`${prefix}${suffix}`)}group(?:\\\\/(\\\\S*))?$`);\n  const ignoredPeers = getCachedRegex(`^${escapeForRegex(`${prefix}${suffix}`)}peer(?:\\\\/(\\\\S*))?$`);\n\n  const customComponentClassRegexes = getCustomComponentClassRegexes(ctx);\n\n  for(const literal of literals){\n\n    const classes = splitClasses(literal.content);\n\n    const { unknownClasses, warnings } = getUnknownClasses(async(ctx), classes);\n\n    if(unknownClasses.length === 0){\n      continue;\n    }\n\n    lintClasses(ctx, literal, className => {\n\n      if(!unknownClasses.includes(className)){\n        return;\n      }\n\n      if(\n        ignore.some(ignoredClass => getCachedRegex(ignoredClass).test(className)) ||\n        customComponentClassRegexes?.some(customComponentClassesRegex => customComponentClassesRegex.test(className)) ||\n        ignoredGroups.test(className) ||\n        ignoredPeers.test(className)\n      ){\n        return;\n      }\n\n      return {\n        data: {\n          className\n        },\n        id: \"unknown\",\n        warnings\n      } as const;\n\n    });\n  }\n}\n\nfunction getCustomComponentClassRegexes(ctx: Context<typeof noUnknownClasses>): RegExp[] | undefined {\n  const { detectComponentClasses } = ctx.options;\n\n  if(!detectComponentClasses){\n    return;\n  }\n\n  const { customComponentClasses } = getCustomComponentClasses(async(ctx));\n  const { prefix, suffix } = getPrefix(async(ctx));\n\n  return customComponentClasses.map(className => getCachedRegex(`^${escapeForRegex(`${prefix}${suffix}`)}(?:.*:)?${escapeForRegex(className)}$`));\n}\n"
  },
  {
    "path": "src/rules/no-unnecessary-whitespace.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { dedent } from \"better-tailwindcss:tests/utils/template.js\";\n\n\ndescribe(noUnnecessaryWhitespace.name, () => {\n\n  it(\"should trim leading and trailing white space in literals\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img class=\"  b  a  \" />`,\n          angularOutput: `<img class=\"b a\" />`,\n          html: `<img class=\"  b  a  \" />`,\n          htmlOutput: `<img class=\"b a\" />`,\n          jsx: `() => <img class=\"  b  a  \" />`,\n          jsxOutput: `() => <img class=\"b a\" />`,\n          svelte: `<img class=\"  b  a  \" />`,\n          svelteOutput: `<img class=\"b a\" />`,\n          vue: `<template><img class=\"  b  a  \" /></template>`,\n          vueOutput: `<template><img class=\"b a\" /></template>`,\n\n          errors: 3\n        }\n      ]\n    });\n  });\n\n  it(\"should trim unnecessary whitespace in concatenated strings\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img [class]=\"'  a  ' + '  b  '\" />`,\n          angularOutput: `<img [class]=\"'a ' + ' b'\" />`,\n          jsx: `() => <img class={\"  a  \" + \"  b  \"} />`,\n          jsxOutput: `() => <img class={\"a \" + \" b\"} />`,\n          svelte: `<img class={\"  a  \" + \"  b  \"} />`,\n          svelteOutput: `<img class={\"a \" + \" b\"} />`,\n          vue: `<template><img :class=\"'  a  ' + '  b  '\" /></template>`,\n          vueOutput: `<template><img :class=\"'a ' + ' b'\" /></template>`,\n\n          errors: 4\n        },\n        {\n          angular: `<img [class]=\"'  a  ' + '  b  ' + '  c  '\" />`,\n          angularOutput: `<img [class]=\"'a ' + ' b ' + ' c'\" />`,\n          jsx: `() => <img class={\"  a  \" + \"  b  \" + \"  c  \"} />`,\n          jsxOutput: `() => <img class={\"a \" + \" b \" + \" c\"} />`,\n          svelte: `<img class={\"  a  \" + \"  b  \" + \"  c  \"} />`,\n          svelteOutput: `<img class={\"a \" + \" b \" + \" c\"} />`,\n          vue: `<template><img :class=\"'  a  ' + '  b  ' + '  c  '\" /></template>`,\n          vueOutput: `<template><img :class=\"'a ' + ' b ' + ' c'\" /></template>`,\n\n          errors: 6\n        }\n      ],\n      valid: [\n        {\n          angular: `<img [class]=\"'a ' + ' b'\" />`,\n          jsx: `() => <img class={\"a \" + \" b\"} />`,\n          svelte: `<img class={\"a \" + \" b\"} />`,\n          vue: `<template><img :class=\"'a ' + ' b'\" /></template>`\n        },\n        {\n          angular: `<img [class]=\"'a ' + ' b ' + ' c'\" />`,\n          jsx: `() => <img class={\"a \" + \" b \" + \" c\"} />`,\n          svelte: `<img class={\"a \" + \" b \" + \" c\"} />`,\n          vue: `<template><img :class=\"'a ' + ' b ' + ' c'\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should trim unnecessary whitespace in conditionally concatenated strings\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img [class]=\"'  a  ' + (someVar ? '  b  ' : '  c  ')\" />`,\n          angularOutput: `<img [class]=\"'a ' + (someVar ? ' b' : ' c')\" />`,\n          jsx: `() => <img class={\"  a  \" + (someVar ? \"  b  \" : \"  c  \")} />`,\n          jsxOutput: `() => <img class={\"a \" + (someVar ? \" b\" : \" c\")} />`,\n          svelte: `<img class={\"  a  \" + (someVar ? \"  b  \" : \"  c  \")} />`,\n          svelteOutput: `<img class={\"a \" + (someVar ? \" b\" : \" c\")} />`,\n          vue: `<template><img :class=\"'  a  ' + (someVar ? '  b  ' : '  c  ')\" /></template>`,\n          vueOutput: `<template><img :class=\"'a ' + (someVar ? ' b' : ' c')\" /></template>`,\n\n          errors: 6\n        }\n      ],\n      valid: [\n        {\n          angular: `<img [class]=\"'a ' + (someVar ? ' b' : ' c')\" />`,\n          jsx: `() => <img class={\"a \" + (someVar ? \" b\" : \" c\")} />`,\n          svelte: `<img class={\"a \" + (someVar ? \" b\" : \" c\")} />`,\n          vue: `<template><img :class=\"'a ' + (someVar ? ' b' : ' c')\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should trim unnecessary whitespace in conditionally concatenated template literal strings\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: '<img [class]=\"`  a  ` + (someVar ? `  b  ` : `  c  `)\" />',\n          angularOutput: '<img [class]=\"`a ` + (someVar ? ` b` : ` c`)\" />',\n          jsx: \"() => <img class={`  a  ` + (someVar ? `  b  ` : `  c  `)} />\",\n          jsxOutput: \"() => <img class={`a ` + (someVar ? ` b` : ` c`)} />\",\n          svelte: \"<img class={`  a  ` + (someVar ? `  b  ` : `  c  `)} />\",\n          svelteOutput: \"<img class={`a ` + (someVar ? ` b` : ` c`)} />\",\n          vue: '<template><img :class=\"`  a  ` + (someVar ? `  b  ` : `  c  `)\" /></template>',\n          vueOutput: '<template><img :class=\"`a ` + (someVar ? ` b` : ` c`)\" /></template>',\n\n          errors: 6\n        }\n      ],\n      valid: [\n        {\n          angular: '<img [class]=\"`a ` + (someVar ? ` b` : ` c`)\" />',\n          jsx: \"() => <img class={`a ` + (someVar ? ` b` : ` c`)} />\",\n          svelte: \"<img class={`a ` + (someVar ? ` b` : ` c`)} />\",\n          vue: '<template><img :class=\"`a ` + (someVar ? ` b` : ` c`)\" /></template>'\n        }\n      ]\n    });\n  });\n\n  it(\"should remove whitespace in empty strings\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img class=\"  \" />`,\n          angularOutput: `<img class=\"\" />`,\n          html: `<img class=\"  \" />`,\n          htmlOutput: `<img class=\"\" />`,\n          jsx: `() => <img class=\"  \" />`,\n          jsxOutput: `() => <img class=\"\" />`,\n          svelte: `<img class=\"  \" />`,\n          svelteOutput: `<img class=\"\" />`,\n          vue: `<template><img class=\"  \" /></template>`,\n          vueOutput: `<template><img class=\"\" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should not report on empty strings\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          angular: `<img class=\"\" />`,\n          html: `<img class=\"\" />`,\n          jsx: `() => <img class=\"\" />`,\n          svelte: `<img class=\"\" />`,\n          vue: `<template><img class=\"\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should collapse empty multiline strings\", () => {\n    const dirtyEmptyMultilineString = `\n\n    `;\n    const cleanEmptyMultilineString = \"\";\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img class=\"${dirtyEmptyMultilineString}\" />`,\n          angularOutput: `<img class=\"${cleanEmptyMultilineString}\" />`,\n          html: `<img class=\"${dirtyEmptyMultilineString}\" />`,\n          htmlOutput: `<img class=\"${cleanEmptyMultilineString}\" />`,\n          jsx: `() => <img class=\"${dirtyEmptyMultilineString}\" />`,\n          jsxOutput: `() => <img class=\"${cleanEmptyMultilineString}\" />`,\n          svelte: `<img class=\"${dirtyEmptyMultilineString}\" />`,\n          svelteOutput: `<img class=\"${cleanEmptyMultilineString}\" />`,\n          vue: `<template><img class=\"${dirtyEmptyMultilineString}\" /></template>`,\n          vueOutput: `<template><img class=\"${cleanEmptyMultilineString}\" /></template>`,\n\n          errors: 1\n        }\n      ]\n    });\n  });\n\n  it(\"should keep the quotes as they are\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img class=\"  b  a  \" />`,\n          angularOutput: `<img class=\"b a\" />`,\n          html: `<img class=\"  b  a  \" />`,\n          htmlOutput: `<img class=\"b a\" />`,\n          jsx: `() => <img class=\"  b  a  \" />`,\n          jsxOutput: `() => <img class=\"b a\" />`,\n          svelte: `<img class=\"  b  a  \" />`,\n          svelteOutput: `<img class=\"b a\" />`,\n          vue: `<template><img class=\"  b  a  \" /></template>`,\n          vueOutput: `<template><img class=\"b a\" /></template>`,\n\n          errors: 3\n        },\n        {\n          angular: `<img class='  b  a  ' />`,\n          angularOutput: `<img class='b a' />`,\n          html: `<img class='  b  a  ' />`,\n          htmlOutput: `<img class='b a' />`,\n          jsx: `() => <img class='  b  a  ' />`,\n          jsxOutput: `() => <img class='b a' />`,\n          svelte: `<img class='  b  a  ' />`,\n          svelteOutput: `<img class='b a' />`,\n          vue: `<template><img class='  b  a  ' /></template>`,\n          vueOutput: `<template><img class='b a' /></template>`,\n\n          errors: 3\n        },\n        {\n          jsx: `() => <img class={\\`  b  a  \\`} />`,\n          jsxOutput: `() => <img class={\\`b a\\`} />`,\n          svelte: `<img class={\\`  b  a  \\`} />`,\n          svelteOutput: `<img class={\\`b a\\`} />`,\n\n          errors: 3\n        },\n        {\n          jsx: `() => <img class={\"  b  a  \"} />`,\n          jsxOutput: `() => <img class={\"b a\"} />`,\n          svelte: `<img class={\"  b  a  \"} />`,\n          svelteOutput: `<img class={\"b a\"} />`,\n\n          errors: 3\n        },\n        {\n          jsx: `() => <img class={'  b  a  '} />`,\n          jsxOutput: `() => <img class={'b a'} />`,\n          svelte: `<img class={'  b  a  '} />`,\n          svelteOutput: `<img class={'b a'} />`,\n\n          errors: 3\n        }\n      ]\n    });\n  });\n\n  it(\"should keep one whitespace around template elements\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`  b  a  \\${\"  c  \"}  d  \\`} />`,\n          jsxOutput: `() => <img class={\\`b a \\${\"c\"} d\\`} />`,\n          svelte: `<img class={\\`  b  a  \\${\"  c  \"}  d  \\`} />`,\n          svelteOutput: `<img class={\\`b a \\${\"c\"} d\\`} />`,\n\n          errors: 7\n        }\n      ]\n    });\n  });\n\n  it(\"should keep no whitespace at the end of the line in multiline strings\", () => {\n\n    const dirty = dedent`\n      a      \n      b  \n      c    \n    `;\n\n    const clean = dedent`\n      a\n      b\n      c\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img class=\"${dirty}\" />`,\n          angularOutput: `<img class=\"${clean}\" />`,\n          html: `<img class=\"${dirty}\" />`,\n          htmlOutput: `<img class=\"${clean}\" />`,\n          jsx: `() => <img class={\\`${dirty}\\`} />`,\n          jsxOutput: `() => <img class={\\`${clean}\\`} />`,\n          svelte: `<img class={\\`${dirty}\\`} />`,\n          svelteOutput: `<img class={\\`${clean}\\`} />`,\n          vue: `<template><img class=\"${dirty}\" /></template>`,\n          vueOutput: `<template><img class=\"${clean}\" /></template>`,\n\n          errors: 3\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove unnecessary whitespace inside and around multiline template literal elements\", () => {\n\n    const dirtyExpression = \"${true ? '  true  ' : '  false  '}\";\n    const cleanExpression = \"${true ? 'true' : 'false'}\";\n\n    const dirtyExpressionAtStart = dedent`\n      ${dirtyExpression}  \n      a  \n    `;\n    const cleanExpressionAtStart = dedent`\n      ${cleanExpression}\n      a\n    `;\n\n    const dirtyExpressionBetween = dedent`\n      a  \n      ${dirtyExpression}  \n      b  \n    `;\n    const cleanExpressionBetween = dedent`\n      a\n      ${cleanExpression}\n      b\n    `;\n\n    const dirtyExpressionAtEnd = dedent`\n      a  \n      ${dirtyExpression}  \n    `;\n    const cleanExpressionAtEnd = dedent`\n      a\n      ${cleanExpression}\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionAtStart}\\`} />`,\n\n          errors: 6\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionBetween}\\`} />`,\n\n          errors: 7\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionAtEnd}\\`} />`,\n\n          errors: 6\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove unnecessary whitespace inside and around single line template literal elements\", () => {\n\n    const dirtyExpression = \"${true ? ' true ' : ' false '}\";\n    const cleanExpression = \"${true ? 'true' : 'false'}\";\n\n    const dirtyExpressionAtStartAtStart = `  ${dirtyExpression}  a  `;\n    const cleanExpressionAtStart = `${cleanExpression} a`;\n\n    const dirtyExpressionBetween = `  a  ${dirtyExpression}  b  `;\n    const cleanExpressionBetween = `a ${cleanExpression} b`;\n\n    const dirtyExpressionAtEnd = `  a  ${dirtyExpression}  `;\n    const cleanExpressionAtEnd = `a ${cleanExpression}`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionAtStartAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionAtStartAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionAtStart}\\`} />`,\n\n          errors: 7\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionBetween}\\`} />`,\n\n          errors: 8\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanExpressionAtEnd}\\`} />`,\n\n          errors: 7\n        }\n      ]\n    });\n\n  });\n\n  it(\"should not create a whitespace around sticky template literal elements\", () => {\n\n    const dirtyExpression = \"${true ? ' true ' : ' false '}\";\n    const cleanExpression = \"${true ? 'true' : 'false'}\";\n\n    const dirtyStickyExpressionAtStart = `  ${dirtyExpression}a  b  `;\n    const cleanStickyExpressionAtStart = `${cleanExpression}a b`;\n\n    const dirtyStickyExpressionBetween = `  a  b${dirtyExpression}c  d  `;\n    const cleanStickyExpressionBetween = `a b${cleanExpression}c d`;\n\n    const dirtyStickyExpressionAtEnd = `  a${dirtyExpression}  `;\n    const cleanStickyExpressionAtEnd = `a${cleanExpression}`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtStart}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtStart}\\`} />`,\n\n          errors: 7\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionBetween}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionBetween}\\`} />`,\n\n          errors: 8\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n          svelte: `<img class={\\`${dirtyStickyExpressionAtEnd}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanStickyExpressionAtEnd}\\`} />`,\n\n          errors: 6\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove newlines whenever possible\", () => {\n    const uncleanedMultilineString = `\n      d  c\n      b  a\n    `;\n\n    const cleanedMultilineString = `\n      d c\n      b a\n    `;\n\n    const cleanedSinglelineString = \"d c b a\";\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          angular: `<img class=\"${uncleanedMultilineString}\" />`,\n          angularOutput: `<img class=\"${cleanedMultilineString}\" />`,\n          html: `<img class=\"${uncleanedMultilineString}\" />`,\n          htmlOutput: `<img class=\"${cleanedMultilineString}\" />`,\n          svelte: `<img class=\"${uncleanedMultilineString}\" />`,\n          svelteOutput: `<img class=\"${cleanedMultilineString}\" />`,\n          vue: `<template><img class=\"${uncleanedMultilineString}\" /></template>`,\n          vueOutput: `<template><img class=\"${cleanedMultilineString}\" /></template>`,\n\n          errors: 2\n        },\n        {\n          angular: `<img class='${uncleanedMultilineString}' />`,\n          angularOutput: `<img class='${cleanedMultilineString}' />`,\n          html: `<img class='${uncleanedMultilineString}' />`,\n          htmlOutput: `<img class='${cleanedMultilineString}' />`,\n          svelte: `<img class='${uncleanedMultilineString}' />`,\n          svelteOutput: `<img class='${cleanedMultilineString}' />`,\n          vue: `<template><img class='${uncleanedMultilineString}' /></template>`,\n          vueOutput: `<template><img class='${cleanedMultilineString}' /></template>`,\n\n          errors: 2\n        },\n        {\n          jsx: `() => <img class={\\`${uncleanedMultilineString}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanedMultilineString}\\`} />`,\n          svelte: `<img class={\\`${uncleanedMultilineString}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanedMultilineString}\\`} />`,\n\n          errors: 2\n        },\n        {\n          angular: `<img class='${uncleanedMultilineString}' />`,\n          angularOutput: `<img class='${cleanedSinglelineString}' />`,\n          html: `<img class='${uncleanedMultilineString}' />`,\n          htmlOutput: `<img class='${cleanedSinglelineString}' />`,\n          jsx: `() => <img class={\\`${uncleanedMultilineString}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanedSinglelineString}\\`} />`,\n          svelte: `<img class={\\`${uncleanedMultilineString}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanedSinglelineString}\\`} />`,\n          vue: `<template><img class='${uncleanedMultilineString}' /></template>`,\n          vueOutput: `<template><img class='${cleanedSinglelineString}' /></template>`,\n\n          errors: 5,\n          options: [{ allowMultiline: false }]\n        }\n      ],\n      valid: [\n        {\n          angular: `<img class=\"${cleanedMultilineString}\" />`,\n          html: `<img class=\"${cleanedMultilineString}\" />`,\n          jsx: `() => <img class={\\`${cleanedMultilineString}\\`} />`,\n          svelte: `<img class=\"${cleanedMultilineString}\" />`,\n          vue: `<template><img class=\"${cleanedMultilineString}\" /></template>`\n        },\n        {\n          angular: `<img class=\"${cleanedSinglelineString}\" />`,\n          html: `<img class=\"${cleanedSinglelineString}\" />`,\n          jsx: `() => <img class=\"${cleanedSinglelineString}\" />`,\n          svelte: `<img class=\"${cleanedSinglelineString}\" />`,\n          vue: `<template><img class=\"${cleanedSinglelineString}\" /></template>`\n        }\n      ]\n    });\n  });\n\n  it(\"should remove unnecessary whitespace in defined call signature arguments\", () => {\n\n    const dirtyDefined = \"defined('  f  e  ');\";\n    const cleanDefined = \"defined('f e');\";\n    const dirtyUndefined = \"notDefined(\\\"  f  e  \\\");\";\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirtyDefined,\n          jsxOutput: cleanDefined,\n          svelte: `<script>${dirtyDefined}</script>`,\n          svelteOutput: `<script>${cleanDefined}</script>`,\n          vue: `<script>${dirtyDefined}</script>`,\n          vueOutput: `<script>${cleanDefined}</script>`,\n\n          errors: 3,\n          options: [{ callees: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: dirtyUndefined,\n          svelte: `<script>${dirtyUndefined}</script>`,\n          vue: `<script>${dirtyUndefined}</script>`,\n\n          options: [{ callees: [\"defined\"] }]\n        }\n      ]\n    });\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirtyDefined,\n          jsxOutput: cleanDefined,\n          svelte: `<script>${dirtyDefined}</script>`,\n          svelteOutput: `<script>${cleanDefined}</script>`,\n          vue: `<script>${dirtyDefined}</script>`,\n          vueOutput: `<script>${cleanDefined}</script>`,\n\n          errors: 3,\n          options: [{ callees: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: dirtyUndefined,\n          svelte: `<script>${dirtyUndefined}</script>`,\n          vue: `<script>${dirtyUndefined}</script>`,\n\n          options: [{ callees: [\"defined\"] }]\n        }\n      ]\n    });\n\n  });\n\n  it(\"should also work in defined call signature arguments in template literals\", () => {\n\n    const dirtyDefined = \"${defined('  f  e  ')}\";\n    const cleanDefined = \"${defined('f e')}\";\n    const dirtyUndefined = \"${notDefined('  f  e  ')}\";\n\n    const dirtyDefinedMultiline = `\n      b a\n      d c ${dirtyDefined} h g\n      j i\n    `;\n\n    const cleanDefinedMultiline = `\n      b a\n      d c ${cleanDefined} h g\n      j i\n    `;\n\n    const dirtyUndefinedMultiline = `\n      b a\n      d c ${dirtyUndefined} h g\n      j i\n    `;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: `() => <img class={\\`${dirtyDefinedMultiline}\\`} />`,\n          jsxOutput: `() => <img class={\\`${cleanDefinedMultiline}\\`} />`,\n          svelte: `<img class={\\`${dirtyDefinedMultiline}\\`} />`,\n          svelteOutput: `<img class={\\`${cleanDefinedMultiline}\\`} />`,\n\n          errors: 3,\n          options: [{ callees: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: `() => <img class={\\`${dirtyUndefinedMultiline}\\`} />`,\n          svelte: `<img class={\\`${dirtyUndefinedMultiline}\\`} />`\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove unnecessary whitespace in string literals in defined variable declarations\", () => {\n\n    const dirtyDefined = \"const defined = \\\"  b  a  \\\";\";\n    const cleanDefined = \"const defined = \\\"b a\\\";\";\n    const dirtyUndefined = \"const notDefined = \\\"  b  a  \\\";\";\n\n    const dirtyMultiline = `const defined = \\`\n      b  a\n      d  c\n    \\`;`;\n\n    const cleanMultiline = `const defined = \\`\n      b a\n      d c\n    \\`;`;\n\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: dirtyDefined,\n          jsxOutput: cleanDefined,\n          svelte: `<script>${dirtyDefined}</script>`,\n          svelteOutput: `<script>${cleanDefined}</script>`,\n          vue: `<script>${dirtyDefined}</script>`,\n          vueOutput: `<script>${cleanDefined}</script>`,\n\n          errors: 3,\n          options: [{ variables: [\"defined\"] }]\n        },\n        {\n          jsx: dirtyMultiline,\n          jsxOutput: cleanMultiline,\n          svelte: `<script>${dirtyMultiline}</script>`,\n          svelteOutput: `<script>${cleanMultiline}</script>`,\n          vue: `<script>${dirtyMultiline}</script>`,\n          vueOutput: `<script>${cleanMultiline}</script>`,\n\n          errors: 2,\n          options: [{ variables: [\"defined\"] }]\n        }\n      ],\n      valid: [\n        {\n          jsx: dirtyUndefined,\n          svelte: `<script>${dirtyUndefined}</script>`,\n          vue: `<script>${dirtyUndefined}</script>`\n        }\n      ]\n    });\n\n  });\n\n  it(\"should remove unnecessary whitespace in string literals in defined tagged template literals\", () => {\n    lint(\n      noUnnecessaryWhitespace,\n\n      {\n        invalid: [\n          {\n            jsx: \"defined`  b   a  `\",\n            jsxOutput: \"defined`b a`\",\n            svelte: \"<script>defined`  b   a  `</script>\",\n            svelteOutput: \"<script>defined`b a`</script>\",\n            vue: \"defined`  b   a  `\",\n            vueOutput: \"defined`b a`\",\n\n            errors: 3,\n            options: [{ tags: [\"defined\"] }]\n          }\n        ],\n        valid: [\n          {\n            jsx: \"notDefined`  b   a  `\",\n            svelte: \"<script>notDefined`  b   a  `</script>\",\n            vue: \"notDefined`  b   a  `\",\n\n            options: [{ tags: [\"defined\"] }]\n          }\n        ]\n      }\n    );\n  });\n\n  // #144\n  it(\"should not remove the whitespace between two template literals\", () => {\n    lint(noUnnecessaryWhitespace, {\n      valid: [\n        {\n          angular: \"<img class=\\\"{{`${'a'} ${'b'}`}}\\\" />\",\n          jsx: \"() => <img class={`${'a'} ${'b'}`} />\",\n          svelte: \"<img class={`${'a'} ${'b'}`} />\",\n          vue: \"<template><img :class=\\\"`${'a'} ${'b'}`\\\" /></template>\"\n        },\n        {\n          angular: \"<img class=\\\"{{`a ${'b'} ${'c'} d`}}\\\" />\",\n          jsx: \"() => <img class={`a ${'b'} ${'c'} d`} />\",\n          svelte: \"<img class={`a ${'b'} ${'c'} d`} />\",\n          vue: \"<template><img class=\\\"`a ${'b'} ${'c'} d`\\\" /></template>\"\n        },\n        {\n          angular: \"<img class=\\\"{{`a ${'b'} c ${'d'} e`}}\\\" />\",\n          jsx: \"() => <img class={`a ${'b'} c ${'d'} e`} />\",\n          svelte: \"<img class={`a ${'b'} c ${'d'} e`} />\",\n          vue: \"<template><img class=\\\"`a ${'b'} c ${'d'} e`\\\" /></template>\"\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/rules/no-unnecessary-whitespace.ts",
    "content": "import { boolean, description, optional, pipe, strictObject } from \"valibot\";\n\nimport { createRule } from \"better-tailwindcss:utils/rule.js\";\nimport { splitClasses, splitWhitespaces } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const noUnnecessaryWhitespace = createRule({\n  autofix: true,\n  category: \"stylistic\",\n  description: \"Disallow unnecessary whitespace between Tailwind CSS classes.\",\n  docs: \"https://github.com/schoero/eslint-plugin-better-tailwindcss/blob/main/docs/rules/no-unnecessary-whitespace.md\",\n  name: \"no-unnecessary-whitespace\",\n  recommended: true,\n\n  messages: {\n    unnecessary: \"Unnecessary whitespace.\"\n  },\n\n  schema: strictObject({\n    allowMultiline: optional(pipe(\n      boolean(),\n      description(\"Allow multi-line class declarations. If this option is disabled, template literal strings will be collapsed into a single line string wherever possible. Must be set to `true` when used in combination with [better-tailwindcss/enforce-consistent-line-wrapping](./enforce-consistent-line-wrapping.md).\")\n    ), true)\n  }),\n\n  lintLiterals: (ctx, literals) => lintLiterals(ctx, literals)\n});\n\nfunction lintLiterals(ctx: Context<typeof noUnnecessaryWhitespace>, literals: Literal[]) {\n\n  const { allowMultiline } = ctx.options;\n\n  for(const literal of literals){\n\n    const classChunks = splitClasses(literal.content);\n    const whitespaceChunks = splitWhitespaces(literal.content);\n\n    for(let whitespaceIndex = 0, stringIndex = 0; whitespaceIndex < whitespaceChunks.length; whitespaceIndex++){\n\n      const isFirstChunk = whitespaceIndex === 0;\n      const isLastChunk = whitespaceIndex === whitespaceChunks.length - 1;\n\n      const startIndex = stringIndex + (literal.openingQuote?.length || 0) + (literal.closingBraces?.length || 0);\n\n      const whitespace = whitespaceChunks[whitespaceIndex];\n\n      stringIndex += whitespace.length;\n\n      const endIndex = startIndex + whitespace.length;\n\n      const className = classChunks[whitespaceIndex] ?? \"\";\n\n      stringIndex += className.length;\n\n      const [literalStart] = literal.range;\n      const keepLeadingWhitespace = literal.isConcatenatedLeft === true;\n      const keepTrailingWhitespace = literal.isConcatenatedRight === true;\n\n      // whitespaces only\n      if(classChunks.length === 0 && !literal.closingBraces && !literal.openingBraces){\n        if(keepLeadingWhitespace || keepTrailingWhitespace){\n          if(whitespace.length <= 1){\n            continue;\n          }\n\n          ctx.report({\n            fix: \" \",\n            id: \"unnecessary\",\n            range: [\n              literalStart + startIndex,\n              literalStart + endIndex\n            ]\n          });\n          continue;\n        }\n\n        if(whitespace === \"\"){\n          continue;\n        }\n\n        ctx.report({\n          fix: \"\",\n          id: \"unnecessary\",\n          range: [\n            literalStart + startIndex,\n            literalStart + endIndex\n          ]\n        });\n        continue;\n      }\n\n      // trailing whitespace before multiline string\n      if(whitespace.includes(\"\\n\") && allowMultiline === true){\n        const whitespaceWithoutLeadingSpaces = whitespace.replace(/^ +/, \"\");\n\n        if(whitespace === whitespaceWithoutLeadingSpaces){\n          continue;\n        }\n\n        ctx.report({\n          fix: whitespaceWithoutLeadingSpaces,\n          id: \"unnecessary\",\n          range: [\n            literalStart + startIndex,\n            literalStart + endIndex\n          ]\n        });\n\n        continue;\n      }\n\n      // whitespace between interpolated literals\n      if(\n        !isFirstChunk && !isLastChunk ||\n        (\n          literal.isInterpolated && literal.closingBraces && isFirstChunk && !isLastChunk ||\n          literal.isInterpolated && literal.openingBraces && isLastChunk && !isFirstChunk ||\n          literal.isInterpolated && literal.closingBraces && literal.openingBraces\n        )\n      ){\n        if(whitespace.length <= 1){\n          continue;\n        }\n\n        ctx.report({\n          fix: \" \",\n          id: \"unnecessary\",\n          range: [\n            literalStart + startIndex,\n            literalStart + endIndex\n          ]\n        });\n\n        continue;\n      }\n\n      // leading or trailing whitespace\n      if(isFirstChunk || isLastChunk){\n        const keepCurrentWhitespace =\n          isFirstChunk && keepLeadingWhitespace ||\n          isLastChunk && keepTrailingWhitespace;\n\n        if(keepCurrentWhitespace){\n          if(whitespace.length <= 1){\n            continue;\n          }\n\n          ctx.report({\n            fix: \" \",\n            id: \"unnecessary\",\n            range: [\n              literalStart + startIndex,\n              literalStart + endIndex\n            ]\n          });\n\n          continue;\n        }\n\n        if(whitespace === \"\"){\n          continue;\n        }\n\n        ctx.report({\n          fix: \"\",\n          id: \"unnecessary\",\n          range: [\n            literalStart + startIndex,\n            literalStart + endIndex\n          ]\n        });\n\n        continue;\n      }\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "src/tailwindcss/canonical-classes.async.v4.ts",
    "content": "import { getUnknownClasses } from \"./unknown-classes.async.v4.js\";\n\nimport type { CanonicalClasses, CanonicalClassOptions } from \"./canonical-classes.js\";\n\n\nexport function getCanonicalClasses(tailwindContext: any, classes: string[], options: CanonicalClassOptions): CanonicalClasses {\n  const result: CanonicalClasses = {};\n\n  if(typeof tailwindContext?.canonicalizeCandidates !== \"function\"){\n    for(const className of classes){\n      result[className] = {\n        input: [className],\n        output: className\n      };\n    }\n    return result;\n  }\n\n  // tailwind currently crashes when unknown classes are passed to canonicalizeCandidates\n  const unknownClasses = getUnknownClasses(tailwindContext, classes);\n  const knownClasses = classes.filter(className => !unknownClasses.includes(className));\n\n  const canonicalizedClasses = tailwindContext.canonicalizeCandidates?.(knownClasses, options);\n\n  const removedClasses = knownClasses.filter(className => !canonicalizedClasses.includes(className));\n  const originalClasses = knownClasses.filter(className => canonicalizedClasses.includes(className));\n  const canonicalClasses = canonicalizedClasses.filter(className => !classes.includes(className));\n\n  for(const originalClass of originalClasses){\n    result[originalClass] = {\n      input: [originalClass],\n      output: originalClass\n    };\n  }\n\n  for(const unknownClass of unknownClasses){\n    result[unknownClass] = {\n      input: [unknownClass],\n      output: unknownClass\n    };\n  }\n\n  if(canonicalClasses.length === 0){\n    return result;\n  }\n\n  for(const canonicalClass of canonicalClasses){\n    const necessaryClasses = removedClasses.filter(removedClass => {\n      const subset = removedClasses.filter(className => className !== removedClass);\n      const subsetCanonical = tailwindContext.canonicalizeCandidates(\n        subset,\n        options\n      );\n      return !subsetCanonical.includes(canonicalClass);\n    });\n\n    for(const originalClass of necessaryClasses){\n      result[originalClass] = {\n        input: necessaryClasses,\n        output: canonicalClass\n      };\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "src/tailwindcss/canonical-classes.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type CanonicalClasses = {\n  [originalClass: string]: {\n    input: string[];\n    output: string;\n  };\n};\n\nexport type CanonicalClassOptions = {\n  collapse: boolean | undefined;\n  logicalToPhysical: boolean | undefined;\n  rem: number | undefined;\n};\n\nexport type GetCanonicalClasses = (ctx: AsyncContext, classes: string[], options: CanonicalClassOptions) => {\n  canonicalClasses: CanonicalClasses;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getCanonicalClasses: GetCanonicalClasses = () => { throw new Error(\"getCanonicalClasses() called before being initialized\"); };\n\nexport function createGetCanonicalClasses(ctx: Context): GetCanonicalClasses {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getCanonicalClasses = (ctx, classes, options) => runWorker(\"getCanonicalClasses\", ctx, classes, options);\n\n  return getCanonicalClasses;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/class-order.async.v3.ts",
    "content": "import type { ClassOrder } from \"./class-order.js\";\n\n\nexport function getClassOrder(tailwindContext: any, classes: string[]): ClassOrder {\n  return tailwindContext.getClassOrder(classes);\n}\n"
  },
  {
    "path": "src/tailwindcss/class-order.async.v4.ts",
    "content": "import type { ClassOrder } from \"./class-order.js\";\n\n\nexport function getClassOrder(tailwindContext: any, classes: string[]): ClassOrder {\n  return tailwindContext.getClassOrder(classes);\n}\n"
  },
  {
    "path": "src/tailwindcss/class-order.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type ClassOrder = [className: string, order: bigint | null][];\n\nexport type GetClassOrder = (ctx: AsyncContext, classes: string[]) => {\n  classOrder: ClassOrder;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getClassOrder: GetClassOrder = () => { throw new Error(\"getClassOrder() called before being initialized\"); };\n\nexport function createGetClassOrder(ctx: Context): GetClassOrder {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getClassOrder = (ctx, classes) => runWorker(\"getClassOrder\", ctx, classes);\n\n  return getClassOrder;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/conflicting-classes.async.v4.ts",
    "content": "import type { ConflictingClasses } from \"./conflicting-classes.js\";\n\n\nexport async function getConflictingClasses(tailwindContext: any, classes: string[]): Promise<ConflictingClasses> {\n  const conflicts: ConflictingClasses = {};\n\n  const classRules = classes.reduce<Record<string, RuleContext>>((classRules, className) => ({\n    ...classRules,\n    [className]: tailwindContext.parseCandidate(className).reduce((classRules, candidate) => {\n      const [rule] = tailwindContext.compileAstNodes(candidate);\n      return {\n        ...classRules,\n        ...getRuleContext(rule?.node?.nodes)\n      };\n    }, {})\n  }), {});\n\n  for(const className in classRules){\n    otherClassLoop: for(const otherClassName in classRules){\n      if(className === otherClassName){\n        continue otherClassLoop;\n      }\n\n      const classRule = classRules[className];\n      const otherClassRule = classRules[otherClassName];\n\n      const paths = Object.keys(classRule);\n      const otherPaths = Object.keys(otherClassRule);\n\n      if(paths.length !== otherPaths.length){\n        continue otherClassLoop;\n      }\n\n      for(const path of paths){\n        for(const otherPath of otherPaths){\n          if(path !== otherPath){\n            continue otherClassLoop;\n          }\n\n          if(classRule[path].length !== otherClassRule[otherPath].length){\n            continue otherClassLoop;\n          }\n\n          for(const classRuleProperty of classRule[path]){\n            if(!otherClassRule[otherPath].find(otherProp => {\n              return otherProp.cssPropertyName === classRuleProperty.cssPropertyName;\n            })){\n              continue otherClassLoop;\n            }\n          }\n\n          for(const otherClassRuleProperty of otherClassRule[otherPath]){\n            conflicts[className] ??= {};\n            conflicts[className][otherClassName] ??= [];\n            conflicts[className][otherClassName].push(otherClassRuleProperty);\n          }\n        }\n      }\n\n    }\n  }\n\n  return conflicts;\n}\n\nexport type StyleRule = {\n  kind: \"rule\";\n  nodes: AstNode[];\n  selector: string;\n};\n\nexport type AtRule = {\n  kind: \"at-rule\";\n  name: string;\n  nodes: AstNode[];\n  params: string;\n};\n\nexport type Declaration = {\n  important: boolean;\n  kind: \"declaration\";\n  property: string;\n  value: string | undefined;\n};\n\nexport type Comment = {\n  kind: \"comment\";\n  value: string;\n};\n\nexport type Context = {\n  context: Record<string, boolean | string>;\n  kind: \"context\";\n  nodes: AstNode[];\n};\n\nexport type AtRoot = {\n  kind: \"at-root\";\n  nodes: AstNode[];\n};\n\nexport type Rule = AtRule | StyleRule;\nexport type AstNode = AtRoot | AtRule | Comment | Context | Declaration | StyleRule;\n\n\ninterface Property {\n  cssPropertyName: string;\n  important: boolean;\n  cssPropertyValue?: string;\n}\n\ninterface RuleContext {\n  [hierarchy: string]: Property[];\n}\n\nfunction getRuleContext(nodes: AstNode[]): RuleContext {\n  const context: RuleContext = {};\n\n  if(!nodes){\n    return context;\n  }\n\n  const checkNested = (nodes: AstNode[], context: RuleContext, path: string = \"\") => {\n    for(const node of nodes.filter(node => !!node)){\n      if(node.kind === \"declaration\"){\n        context[path] ??= [];\n\n        if(node.value === undefined){\n          continue;\n        }\n\n        context[path].push({\n          cssPropertyName: node.property,\n          cssPropertyValue: node.value,\n          important: node.important\n        });\n        continue;\n      }\n\n      if(node.kind === \"rule\"){\n        return void checkNested(node.nodes, context, path + node.selector);\n      }\n\n      if(node.kind === \"at-rule\"){\n        return void checkNested(node.nodes, context, path + node.name + node.params);\n      }\n    }\n  };\n\n  checkNested(nodes, context);\n\n  return context;\n}\n"
  },
  {
    "path": "src/tailwindcss/conflicting-classes.ts",
    "content": "// runner.js\nimport { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type ConflictingClasses = {\n  [className: string]: {\n    [conflictingClassName: string]: {\n      cssPropertyName: string;\n      important: boolean;\n      cssPropertyValue?: string;\n    }[];\n  };\n};\n\nexport type GetConflictingClasses = (ctx: AsyncContext, classes: string[]) => {\n  conflictingClasses: ConflictingClasses;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getConflictingClasses: GetConflictingClasses = () => { throw new Error(\"getConflictingClasses() called before being initialized\"); };\n\nexport function createGetConflictingClasses(ctx: Context): GetConflictingClasses {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getConflictingClasses = (ctx, classes) => runWorker(\"getConflictingClasses\", ctx, classes);\n\n  return getConflictingClasses;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/context.async.v3.ts",
    "content": "import { withCache } from \"../async-utils/cache.js\";\nimport { normalize } from \"../async-utils/path.js\";\n\nimport type { AsyncContext } from \"../utils/context.js\";\n\n\nexport const createTailwindContext = async (ctx: AsyncContext) => withCache(\"tailwind-context\", ctx.tailwindConfigPath, async () => {\n  const { default: defaultConfig } = await import(normalize(`${ctx.installation}/defaultConfig.js`));\n  const setupContextUtils = await import(normalize(`${ctx.installation}/lib/lib/setupContextUtils.js`));\n  const { default: loadConfig } = await import(normalize(`${ctx.installation}/loadConfig.js`));\n  const { default: resolveConfig } = await import(normalize(`${ctx.installation}/resolveConfig.js`));\n\n  const config = resolveConfig(\n    ctx.tailwindConfigPath === \"default\"\n      ? defaultConfig\n      : loadConfig(ctx.tailwindConfigPath)\n  );\n\n  return setupContextUtils.createContext?.(config) ?? setupContextUtils.default?.createContext?.(config);\n});\n"
  },
  {
    "path": "src/tailwindcss/context.async.v4.ts",
    "content": "import { readFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nimport { createJiti } from \"jiti\";\n\nimport { withCache } from \"../async-utils/cache.js\";\nimport { normalize } from \"../async-utils/path.js\";\nimport { resolveCss, resolveJs } from \"../async-utils/resolvers.js\";\n\nimport type { AsyncContext } from \"../utils/context.js\";\n\n\nexport const createTailwindContext = async (ctx: AsyncContext) => withCache(\"tailwind-context\", ctx.tailwindConfigPath, async () => {\n  const jiti = createJiti(getCurrentFilename(), {\n    fsCache: false,\n    moduleCache: false\n  });\n\n  const importBasePath = dirname(ctx.tailwindConfigPath);\n  const tailwindPath = resolveJs(ctx, \"tailwindcss\", importBasePath);\n\n  // eslint-disable-next-line eslint-plugin-typescript/naming-convention\n  const { __unstable__loadDesignSystem } = await import(normalize(tailwindPath));\n\n  const css = await readFile(ctx.tailwindConfigPath, \"utf-8\");\n\n  // Load the design system and set up a compatible context object that is\n  // usable by the rest of the plugin\n  const design = await __unstable__loadDesignSystem(css, {\n    base: importBasePath,\n    loadModule: createLoader(ctx, jiti, {\n      filepath: ctx.tailwindConfigPath,\n      legacy: false,\n      onError: (id, err, resourceType) => {\n        console.error(`Unable to load ${resourceType}: ${id}`, err);\n\n        if(resourceType === \"config\"){\n          return {};\n        } else if(resourceType === \"plugin\"){\n          return () => {};\n        }\n      }\n    }),\n\n    loadStylesheet: async (id: string, base: string) => {\n      try {\n        const resolved = resolveCss(ctx, id, base);\n\n        return {\n          base: dirname(resolved),\n          content: await readFile(resolved, \"utf-8\")\n        };\n      } catch {\n        return {\n          base: \"\",\n          content: \"\"\n        };\n      }\n    }\n  });\n\n  return design;\n});\n\nfunction createLoader<T>(ctx: AsyncContext, jiti: ReturnType<typeof createJiti>, {\n  filepath,\n  legacy,\n  onError\n}: {\n  filepath: string;\n  legacy: boolean;\n  onError: (id: string, error: unknown, resourceType: string) => T;\n}) {\n  const cacheKey = `${+Date.now()}`;\n\n  async function loadFile(id: string, base: string, resourceType: string) {\n    try {\n      const resolved = resolveJs(ctx, id, base);\n\n      const url = pathToFileURL(resolved);\n      url.searchParams.append(\"t\", cacheKey);\n\n      return await jiti.import(url.href, { default: true });\n    } catch (err){\n      return onError(id, err, resourceType);\n    }\n  }\n\n  if(legacy){\n    const baseDir = dirname(filepath);\n    return async (id: string) => loadFile(id, baseDir, \"module\");\n  }\n\n  return async (id: string, base: string, resourceType: string) => {\n    return {\n      base,\n      module: await loadFile(id, base, resourceType)\n    };\n  };\n}\n\nfunction getCurrentFilename() {\n  // eslint-disable-next-line eslint-plugin-typescript/prefer-ts-expect-error\n  // @ts-ignore - `import.meta` doesn't exist in CommonJS -> will be transformed in build step\n  return import.meta.url;\n}\n"
  },
  {
    "path": "src/tailwindcss/custom-component-classes.async.v3.ts",
    "content": "import type { AsyncContext } from \"../utils/context.js\";\nimport type { CustomComponentClasses } from \"./custom-component-classes.js\";\n\n\nexport async function getCustomComponentClasses(_ctx: AsyncContext): Promise<CustomComponentClasses> {\n  return [];\n}\n"
  },
  {
    "path": "src/tailwindcss/custom-component-classes.async.v4.ts",
    "content": "import { readFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nimport { fork } from \"@eslint/css-tree\";\nimport { tailwind4 } from \"tailwind-csstree\";\n\nimport { withCache } from \"../async-utils/cache.js\";\nimport { resolveCss } from \"../async-utils/resolvers.js\";\n\nimport type { CssNode } from \"@eslint/css-tree\";\n\nimport type { AsyncContext } from \"../utils/context.js\";\nimport type { CustomComponentClasses } from \"./custom-component-classes.js\";\n\n\ninterface ImportInfo {\n  path: string;\n  layer?: string | undefined;\n}\n\ninterface CssFile {\n  ast: CssNode;\n  imports: ImportInfo[];\n}\n\ninterface CssFiles {\n  [resolvedPath: string]: CssFile;\n}\n\nconst { findAll, generate, parse, walk } = fork(tailwind4);\n\nexport async function getCustomComponentClasses(ctx: AsyncContext): Promise<CustomComponentClasses> {\n  const resolvedPath = resolveCss(ctx, ctx.tailwindConfigPath);\n\n  if(!resolvedPath){\n    return [];\n  }\n\n  const files = await parseCssFilesDeep(ctx, resolvedPath);\n\n  return getCustomComponentUtilities(files, resolvedPath);\n}\n\nasync function parseCssFilesDeep(ctx: AsyncContext, resolvedPath: string): Promise<CssFiles> {\n  const cssFiles: CssFiles = {};\n\n  const cssFile = await parseCssFile(ctx, resolvedPath);\n\n  if(!cssFile){\n    return cssFiles;\n  }\n\n  cssFiles[resolvedPath] = cssFile;\n\n  for(const { path } of cssFile.imports){\n    const importedFiles = await parseCssFilesDeep(ctx, path);\n\n    for(const importedFile in importedFiles){\n      cssFiles[importedFile] = importedFiles[importedFile];\n    }\n  }\n  return cssFiles;\n}\n\nconst parseCssFile = async (ctx: AsyncContext, resolvedPath: string): Promise<CssFile | undefined> => withCache(\"css-file\", resolvedPath, async () => {\n  try {\n    const content = await readFile(resolvedPath, \"utf-8\");\n    const ast = parse(content);\n\n    const importNodes = findAll(ast, node => node.type === \"Atrule\" &&\n      node.name === \"import\" &&\n      node.prelude?.type === \"AtrulePrelude\");\n\n    const imports = importNodes.reduce<ImportInfo[]>((imports, importNode) => {\n      if(importNode.type !== \"Atrule\" || !importNode.prelude){\n        return imports;\n      }\n\n      const prelude = generate(importNode.prelude);\n      const importStatement = prelude.match(/[\"'](?<importPath>[^\"']+)[\"'](?<rest>.*)/);\n\n      if(!importStatement){\n        return imports;\n      }\n\n      const { importPath, rest } = importStatement.groups || {};\n\n      const layerMatch = rest?.match(/layer(?:\\((?<layerName>[^)]+)\\))?/);\n      const layer = layerMatch ? layerMatch.groups?.layerName || \"anonymous\" : undefined;\n\n      const cwd = dirname(resolvedPath);\n      const resolvedImportPath = resolveCss(ctx, importPath, cwd);\n\n      if(resolvedImportPath){\n        imports.push({ layer, path: resolvedImportPath });\n      }\n\n      return imports;\n    }, []);\n\n    return {\n      ast,\n      imports\n    } satisfies CssFile;\n\n  } catch {}\n});\n\nfunction getCustomComponentUtilities(files: CssFiles, filePath: string, currentLayer: string[] = []): string[] {\n  const classes = new Set<string>();\n  const file = files[filePath];\n\n  if(!file){\n    return [];\n  }\n\n  for(const { layer, path } of file.imports){\n    const nextLayer = [...currentLayer];\n\n    if(layer){\n      nextLayer.push(layer);\n    }\n\n    const importedClasses = getCustomComponentUtilities(files, path, nextLayer);\n\n    for(const importedClass of importedClasses){\n      classes.add(importedClass);\n    }\n  }\n\n  const localLayers: string[] = [];\n\n  walk(file.ast, {\n    enter: (node: CssNode) => {\n      if(node.type === \"Atrule\" && node.name === \"layer\" && node.prelude?.type === \"AtrulePrelude\" && node.block){\n        const layerName = generate(node.prelude).trim();\n        localLayers.push(layerName);\n      }\n\n      if(node.type === \"ClassSelector\"){\n        if([...currentLayer, ...localLayers][0] === \"components\"){\n          classes.add(node.name);\n        }\n      }\n    },\n    leave: (node: CssNode) => {\n      if(node.type === \"Atrule\" && node.name === \"layer\" && node.block){\n        localLayers.pop();\n      }\n    }\n  });\n\n  return Array.from(classes);\n}\n"
  },
  {
    "path": "src/tailwindcss/custom-component-classes.ts",
    "content": "// runner.js\n\n\nimport { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type CustomComponentClasses = string[];\n\nexport type GetCustomComponentClasses = (ctx: AsyncContext) => {\n  customComponentClasses: CustomComponentClasses;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getCustomComponentClasses: GetCustomComponentClasses = () => { throw new Error(\"getCustomComponentClasses() called before being initialized\"); };\n\nexport function createGetCustomComponentClasses(ctx: Context): GetCustomComponentClasses {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getCustomComponentClasses = ctx => runWorker(\"getCustomComponentClasses\", ctx);\n\n  return getCustomComponentClasses;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/dissect-classes.async.v3.ts",
    "content": "import { escapeForRegex } from \"../async-utils/escape.js\";\nimport { normalize } from \"../async-utils/path.js\";\nimport { getCachedRegex } from \"../async-utils/regex.js\";\nimport { getPrefix } from \"./prefix.async.v3.js\";\n\nimport type { AsyncContext } from \"../utils/context.js\";\nimport type { DissectedClass, DissectedClasses } from \"./dissect-classes.js\";\n\n\nexport async function getDissectedClasses(ctx: AsyncContext, tailwindContext: any, classes: string[]): Promise<DissectedClasses> {\n\n  const utils = await import(normalize(`${ctx.installation}/lib/util/splitAtTopLevelOnly.js`));\n\n  const prefix = getPrefix(tailwindContext);\n  const separator = tailwindContext.tailwindConfig.separator ?? \":\";\n\n  return classes.reduce<Record<string, DissectedClass>>((acc, className) => {\n    const splitChunks = utils.splitAtTopLevelOnly?.(className, separator) ?? utils.default?.splitAtTopLevelOnly?.(className, separator);\n    const variants = splitChunks.slice(0, -1);\n\n    let base = className\n      .replace(new RegExp(`^${escapeForRegex(variants.join(separator) + separator)}`), \"\")\n      .replace(new RegExp(`^${escapeForRegex(prefix)}`), \"\");\n\n    const isNegative = base.startsWith(\"-\");\n    base = base.replace(getCachedRegex(/^-/), \"\");\n\n    const isImportantAtStart = base.startsWith(\"!\");\n    base = base.replace(getCachedRegex(/^!/), \"\");\n\n    const isImportantAtEnd = base.endsWith(\"!\");\n    base = base.replace(getCachedRegex(/!$/), \"\");\n\n    acc[className] = {\n      base,\n      className,\n      important: [isImportantAtStart, isImportantAtEnd],\n      negative: isNegative,\n      prefix,\n      separator,\n      variants\n    };\n\n    return acc;\n  }, {});\n}\n"
  },
  {
    "path": "src/tailwindcss/dissect-classes.async.v4.ts",
    "content": "import { escapeForRegex } from \"../async-utils/escape.js\";\nimport { getCachedRegex } from \"../async-utils/regex.js\";\nimport { getPrefix } from \"./prefix.async.v4.js\";\n\nimport type { DissectedClass, DissectedClasses } from \"./dissect-classes.js\";\n\n\nexport function getDissectedClasses(tailwindContext: any, classes: string[]): DissectedClasses {\n  const prefix = getPrefix(tailwindContext);\n  const separator = \":\";\n\n  return classes.reduce<Record<string, DissectedClass>>((acc, className) => {\n    const [parsed] = tailwindContext.parseCandidate(className);\n\n    const variants = parsed?.variants?.map(variant => tailwindContext.printVariant(variant)).reverse();\n\n    let base = className\n      .replace(getCachedRegex(`^${escapeForRegex(prefix + separator)}`), \"\")\n      .replace(getCachedRegex(`^${escapeForRegex((variants?.join(separator) ?? \"\") + separator)}`), \"\");\n\n    const isNegative = base.startsWith(\"-\");\n    base = base.replace(getCachedRegex(/^-/), \"\");\n\n    const isImportantAtStart = base.startsWith(\"!\");\n    base = base.replace(getCachedRegex(/^!/), \"\");\n\n    const isImportantAtEnd = base.endsWith(\"!\");\n    base = base.replace(getCachedRegex(/!$/), \"\");\n\n    acc[className] = {\n      base,\n      className,\n      important: [isImportantAtStart, isImportantAtEnd],\n      negative: isNegative,\n      prefix,\n      separator,\n      variants\n    };\n\n    return acc;\n  }, {});\n}\n"
  },
  {
    "path": "src/tailwindcss/dissect-classes.test.ts",
    "content": "import { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n\nimport { createGetDissectedClasses } from \"better-tailwindcss:tailwindcss/dissect-classes.js\";\nimport { createTestContext } from \"better-tailwindcss:tests/utils/context.js\";\nimport { css } from \"better-tailwindcss:tests/utils/template.js\";\nimport { TestDirectory } from \"better-tailwindcss:tests/utils/tmp.js\";\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version\";\nimport { async } from \"better-tailwindcss:utils/context.js\";\n\n\nfunction dissectClass(className: string) {\n  const ctx = createTestContext();\n  const getDissectedClasses = createGetDissectedClasses(ctx);\n  const { dissectedClasses } = getDissectedClasses(async(ctx), [className]);\n\n  return dissectedClasses[className];\n}\n\ndescribe(\"getDissectedClass\", () => {\n  let fs: TestDirectory;\n\n  beforeEach(() => {\n    fs = new TestDirectory({ \"global.css\": css`@import \"tailwindcss\";` });\n  });\n\n  afterEach(() => {\n    fs.cleanUp();\n  });\n\n  describe(\"variants\", () => {\n    it(\"should not return any variants for a class without variants\", () => {\n      expect(dissectClass(\"text-red-500\").variants).toEqual([]);\n    });\n\n    it(\"should return the variant for a class with a variant\", () => {\n      expect(dissectClass(\"hover:text-red-500\").variants).toEqual([\"hover\"]);\n    });\n\n    it(\"should return multiple variants for a class with multiple variants\", () => {\n      expect(dissectClass(\"lg:hover:text-red-500\").variants).toEqual([\"lg\", \"hover\"]);\n    });\n\n    it(\"should not return any variants for an arbitrary class\", () => {\n      expect(dissectClass(\"[color:red]\").variants).toEqual([]);\n    });\n\n    it(\"should return the variant for an arbitrary class with a variant\", () => {\n      expect(dissectClass(\"hover:[color:red]\").variants).toEqual([\"hover\"]);\n    });\n\n    it(\"should return the variant for an arbitrary variant\", () => {\n      expect(dissectClass(\"[&:hover]:text-red-500\").variants).toEqual([\"[&:hover]\"]);\n    });\n\n    it(\"should return the correct variants for arbitrary variants mixed with normal variants\", () => {\n      expect(dissectClass(\"lg:[&:hover]:text-red-500\").variants).toEqual([\"lg\", \"[&:hover]\"]);\n    });\n\n    it(\"should work with functional variants\", () => {\n      expect(dissectClass(\"aria-disabled:text-red-500\").variants).toEqual([\"aria-disabled\"]);\n      expect(dissectClass(\"aria-[disabled]:text-red-500\").variants).toEqual([\"aria-[disabled]\"]);\n    });\n\n    it(\"should work with compound variants\", () => {\n      expect(dissectClass(\"has-[&_p]:text-red-500\").variants).toEqual([\"has-[&_p]\"]);\n    });\n\n    it(\"should not crash on unknown classes\", () => {\n      expect(() => dissectClass(\"unknown-class\")).not.toThrow();\n      expect(() => dissectClass(\"hover:unknown-class\")).not.toThrow();\n      expect(() => dissectClass(\"lg:hover:unknown-class\")).not.toThrow();\n    });\n  });\n\n  describe(\"important\", () => {\n    it(\"should return true for a class with an important modifier\", () => {\n      expect(dissectClass(\"text-red-500!\").important).toEqual([false, true]);\n      expect(dissectClass(\"!text-red-500\").important).toEqual([true, false]);\n    });\n  });\n\n  describe(\"base\", () => {\n    it.runIf(getTailwindCSSVersion().major >= 4)(\"should return the base class name in tailwind >= 4\", () => {\n      expect(dissectClass(\"text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"hover:text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"lg:hover:text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"lg:hover:text-red-500!\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"lg:hover:text-red-500/50\").base).toBe(\"text-red-500/50\");\n      expect(dissectClass(\"lg:hover:text-red-500/50!\").base).toBe(\"text-red-500/50\");\n    });\n\n    it.runIf(getTailwindCSSVersion().major <= 3)(\"should return the base class name in tailwind <= 3\", () => {\n      expect(dissectClass(\"text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"hover:text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"lg:hover:text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"lg:hover:!text-red-500\").base).toBe(\"text-red-500\");\n      expect(dissectClass(\"lg:hover:text-red-500/50\").base).toBe(\"text-red-500/50\");\n      expect(dissectClass(\"lg:hover:!text-red-500/50\").base).toBe(\"text-red-500/50\");\n    });\n  });\n\n  describe(\"negative\", () => {\n    it(\"should return true for a class with a negative modifier\", () => {\n      expect(dissectClass(\"-top-50\").negative).toBe(true);\n      expect(dissectClass(\"hover:-top-50\").negative).toBe(true);\n      expect(dissectClass(\"lg:hover:-top-50\").negative).toBe(true);\n      expect(dissectClass(\"lg:hover:-top-50!\").negative).toBe(true);\n    });\n\n    it(\"should return false for a class without a negative modifier\", () => {\n      expect(dissectClass(\"top-50\").negative).toBe(false);\n      expect(dissectClass(\"hover:top-50\").negative).toBe(false);\n      expect(dissectClass(\"lg:hover:top-50\").negative).toBe(false);\n      expect(dissectClass(\"lg:hover:top-50!\").negative).toBe(false);\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/tailwindcss/dissect-classes.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport interface DissectedClass {\n  base: string;\n  className: string;\n  important: [start: boolean, end: boolean];\n  negative: boolean;\n  prefix: string;\n  separator: string;\n  /** Will be undefined in tailwindcss 4 for non-tailwind classes. */\n  variants: string[] | undefined;\n}\n\nexport interface DissectedClasses {\n  [className: string]: DissectedClass;\n}\n\nexport type GetDissectedClasses = (ctx: AsyncContext, classes: string[]) => {\n  dissectedClasses: DissectedClasses;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getDissectedClasses: GetDissectedClasses = () => { throw new Error(\"getDissectedClasses() called before being initialized\"); };\n\nexport function createGetDissectedClasses(ctx: Context): GetDissectedClasses {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getDissectedClasses = (ctx, classes) => runWorker(\"getDissectedClasses\", ctx, classes);\n\n  return getDissectedClasses;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/prefix.async.v3.ts",
    "content": "import type { Prefix } from \"./prefix.js\";\n\n\nexport function getPrefix(tailwindContext: any): Prefix {\n  return tailwindContext.tailwindConfig.prefix ?? \"\";\n}\n\nexport function getSuffix(tailwindContext: any): string {\n  return \"\";\n}\n"
  },
  {
    "path": "src/tailwindcss/prefix.async.v4.ts",
    "content": "import type { Prefix, Suffix } from \"./prefix.js\";\n\n\nexport function getPrefix(tailwindContext: any): Prefix {\n  return tailwindContext.theme.prefix ?? \"\";\n}\n\nexport function getSuffix(tailwindContext: any): Suffix {\n  return !!tailwindContext.theme.prefix ? \":\" : \"\";\n}\n"
  },
  {
    "path": "src/tailwindcss/prefix.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type Prefix = string;\nexport type Suffix = string;\n\nexport type GetPrefix = (ctx: AsyncContext) => {\n  prefix: Prefix;\n  suffix: Suffix;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getPrefix: GetPrefix = () => { throw new Error(\"getPrefix() called before being initialized\"); };\n\nexport function createGetPrefix(ctx: Context): GetPrefix {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getPrefix = ctx => runWorker(\"getPrefix\", ctx);\n\n  return getPrefix;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/tailwind.async.worker.v3.ts",
    "content": "import { runAsWorker } from \"synckit\";\n\nimport { getClassOrder } from \"./class-order.async.v3.js\";\nimport { createTailwindContext } from \"./context.async.v3.js\";\nimport { getCustomComponentClasses } from \"./custom-component-classes.async.v3.js\";\nimport { getDissectedClasses } from \"./dissect-classes.async.v3.js\";\nimport { getPrefix, getSuffix } from \"./prefix.async.v3.js\";\nimport { getUnknownClasses } from \"./unknown-classes.async.v3.js\";\nimport { getVariantOrder } from \"./variant-order.async.v3.js\";\n\nimport type { OperationHandlers, Operations } from \"../async-utils/operations.js\";\nimport type { CanonicalClasses } from \"./canonical-classes.js\";\nimport type { ConflictingClasses } from \"./conflicting-classes.js\";\n\n\nconst handlers: OperationHandlers = {\n  getCanonicalClasses: async (ctx, classes, _options) => {\n    const canonicalClasses = classes.reduce<CanonicalClasses>((acc, className) => {\n      acc[className] = {\n        input: [className],\n        output: className\n      };\n      return acc;\n    }, {});\n\n    return { canonicalClasses, warnings: ctx.warnings };\n  },\n  getClassOrder: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const classOrder = getClassOrder(tailwindContext, classes);\n\n    return { classOrder, warnings: ctx.warnings };\n  },\n  getConflictingClasses: async (ctx, _classes) => {\n    const conflictingClasses: ConflictingClasses = {};\n\n    return { conflictingClasses, warnings: ctx.warnings };\n  },\n  getCustomComponentClasses: async ctx => {\n    const customComponentClasses = await getCustomComponentClasses(ctx);\n\n    return { customComponentClasses, warnings: ctx.warnings };\n  },\n  getDissectedClasses: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const dissectedClasses = await getDissectedClasses(ctx, tailwindContext, classes);\n\n    return { dissectedClasses, warnings: ctx.warnings };\n  },\n  getPrefix: async ctx => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const prefix = getPrefix(tailwindContext);\n    const suffix = getSuffix(tailwindContext);\n\n    return { prefix, suffix, warnings: ctx.warnings };\n  },\n  getUnknownClasses: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const unknownClasses = await getUnknownClasses(ctx, tailwindContext, classes);\n\n    return { unknownClasses, warnings: ctx.warnings };\n  },\n  getVariantOrder: async ctx => {\n    const variantOrder = getVariantOrder();\n\n    return { variantOrder, warnings: ctx.warnings };\n  }\n};\n\nrunAsWorker(async <Operation extends keyof Operations>(operation: Operation, ...args: Parameters<Operations[Operation]>) => {\n  return handlers[operation](...args);\n});\n"
  },
  {
    "path": "src/tailwindcss/tailwind.async.worker.v4.ts",
    "content": "import { runAsWorker } from \"synckit\";\n\nimport { getCanonicalClasses } from \"./canonical-classes.async.v4.js\";\nimport { getClassOrder } from \"./class-order.async.v4.js\";\nimport { getConflictingClasses } from \"./conflicting-classes.async.v4.js\";\nimport { createTailwindContext } from \"./context.async.v4.js\";\nimport { getCustomComponentClasses } from \"./custom-component-classes.async.v4.js\";\nimport { getDissectedClasses } from \"./dissect-classes.async.v4.js\";\nimport { getPrefix, getSuffix } from \"./prefix.async.v4.js\";\nimport { getUnknownClasses } from \"./unknown-classes.async.v4.js\";\nimport { getVariantOrder } from \"./variant-order.async.v4.js\";\n\nimport type { OperationHandlers, Operations } from \"../async-utils/operations.js\";\n\n\nconst handlers: OperationHandlers = {\n  getCanonicalClasses: async (ctx, classes, options) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const canonicalClasses = getCanonicalClasses(tailwindContext, classes, options);\n\n    return { canonicalClasses, warnings: ctx.warnings };\n  },\n  getClassOrder: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const classOrder = getClassOrder(tailwindContext, classes);\n\n    return { classOrder, warnings: ctx.warnings };\n  },\n  getConflictingClasses: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const conflictingClasses = await getConflictingClasses(tailwindContext, classes);\n\n    return { conflictingClasses, warnings: ctx.warnings };\n  },\n  getCustomComponentClasses: async ctx => {\n    const customComponentClasses = await getCustomComponentClasses(ctx);\n\n    return { customComponentClasses, warnings: ctx.warnings };\n  },\n  getDissectedClasses: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const dissectedClasses = getDissectedClasses(tailwindContext, classes);\n\n    return { dissectedClasses, warnings: ctx.warnings };\n  },\n  getPrefix: async ctx => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const prefix = getPrefix(tailwindContext);\n    const suffix = getSuffix(tailwindContext);\n\n    return { prefix, suffix, warnings: ctx.warnings };\n  },\n  getUnknownClasses: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const unknownClasses = getUnknownClasses(tailwindContext, classes);\n\n    return { unknownClasses, warnings: ctx.warnings };\n  },\n  getVariantOrder: async (ctx, classes) => {\n    const tailwindContext = await createTailwindContext(ctx);\n    const variantOrder = getVariantOrder(tailwindContext, classes);\n\n    return { variantOrder, warnings: ctx.warnings };\n  }\n};\n\nrunAsWorker(async <Operation extends keyof Operations>(operation: Operation, ...args: Parameters<Operations[Operation]>) => {\n  return handlers[operation](...args);\n});\n"
  },
  {
    "path": "src/tailwindcss/unknown-classes.async.v3.ts",
    "content": "import { normalize } from \"../async-utils/path.js\";\n\nimport type { AsyncContext } from \"../utils/context.js\";\nimport type { UnknownClass } from \"./unknown-classes.js\";\n\n\nexport async function getUnknownClasses(ctx: AsyncContext, tailwindContext: any, classes: string[]): Promise<UnknownClass[]> {\n  const rules = await import(normalize(`${ctx.installation}/lib/lib/generateRules.js`));\n\n  return classes\n    .filter(className => {\n      const generated = rules.generateRules?.([className], tailwindContext) ?? rules.default?.generateRules?.([className], tailwindContext);\n\n      return generated.length === 0;\n    });\n}\n"
  },
  {
    "path": "src/tailwindcss/unknown-classes.async.v4.ts",
    "content": "import type { UnknownClass } from \"./unknown-classes.js\";\n\n\nexport function getUnknownClasses(tailwindContext: any, classes: string[]): UnknownClass[] {\n  const css = tailwindContext.candidatesToCss(classes);\n\n  return classes.filter((_, index) => css.at(index) === null);\n}\n"
  },
  {
    "path": "src/tailwindcss/unknown-classes.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type UnknownClass = string;\n\nexport type GetUnknownClasses = (ctx: AsyncContext, classes: string[]) => {\n  unknownClasses: UnknownClass[];\n  warnings: (Warning | undefined)[];\n};\n\nexport let getUnknownClasses: GetUnknownClasses = () => { throw new Error(\"getUnknownClasses() called before being initialized\"); };\n\nexport function createGetUnknownClasses(ctx: Context): GetUnknownClasses {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getUnknownClasses = (ctx, classes) => runWorker(\"getUnknownClasses\", ctx, classes);\n\n  return getUnknownClasses;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/tailwindcss/variant-order.async.v3.ts",
    "content": "import type { VariantOrder } from \"./variant-order.js\";\n\n\nexport function getVariantOrder(): VariantOrder {\n  return {};\n}\n"
  },
  {
    "path": "src/tailwindcss/variant-order.async.v4.ts",
    "content": "import { VARIANT_ORDER_FLAGS } from \"../async-utils/order.js\";\n\nimport type { VariantOrder } from \"./variant-order.js\";\n\n\nexport function getVariantOrder(tailwindContext: any, classes: string[]): VariantOrder {\n  const candidates = classes.map(className => tailwindContext.parseCandidate(className));\n\n  const variantOrder = tailwindContext.getVariantOrder();\n  const variants = tailwindContext.getVariants();\n\n  const variantsByName = new Map(\n    (variants ?? []).map(variant => {\n      return [variant.name, variant];\n    })\n  );\n  const variantOrderByName = new Map(\n    [...variantOrder.entries()].map(([variant, order]) => {\n      return [tailwindContext.printVariant(variant), order];\n    })\n  );\n\n  return candidates.reduce<VariantOrder>((acc, parsedCandidates) => {\n    for(const candidate of parsedCandidates ?? []){\n      for(const variantCandidate of candidate?.variants ?? []){\n        const variantName = tailwindContext.printVariant(variantCandidate);\n        const variant = variantsByName.get(variantName);\n        const twOrder = variantOrderByName.get(variantName) || 0;\n        const globalOrder = hasGlobalSelector(variant) ? VARIANT_ORDER_FLAGS.GLOBAL : 0;\n\n        acc[variantName] ??= globalOrder | twOrder;\n      }\n    }\n\n    return acc;\n  }, {});\n}\n\n\nfunction hasGlobalSelector(variant: any): boolean {\n  const selectors = variant?.selectors?.();\n\n  if(!Array.isArray(selectors) || selectors.length <= 0){\n    return false;\n  }\n\n  return selectors.every(selector => {\n    return typeof selector === \"string\" && !selector.includes(\"&\");\n  });\n}\n"
  },
  {
    "path": "src/tailwindcss/variant-order.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { createSyncFn } from \"synckit\";\n\nimport { getWorkerOptions } from \"better-tailwindcss:utils/worker.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\nimport type { AsyncContext } from \"better-tailwindcss:utils/context.js\";\n\n\nexport type VariantOrder = Record<string, number | undefined>;\n\nexport type GetVariantOrder = (ctx: AsyncContext, classes: string[]) => {\n  variantOrder: VariantOrder;\n  warnings: (Warning | undefined)[];\n};\n\nexport let getVariantOrder: GetVariantOrder = () => { throw new Error(\"getVariantOrder() called before being initialized\"); };\n\nexport function createGetVariantOrder(ctx: Context): GetVariantOrder {\n  const workerPath = getWorkerPath(ctx);\n  const workerOptions = getWorkerOptions();\n  const runWorker = createSyncFn(workerPath, workerOptions);\n\n  getVariantOrder = (ctx, classes) => runWorker(\"getVariantOrder\", ctx, classes);\n\n  return getVariantOrder;\n}\n\nfunction getWorkerPath(ctx: Context) {\n  return resolve(import.meta.dirname, `./tailwind.async.worker.v${ctx.version.major}.js`);\n}\n"
  },
  {
    "path": "src/types/ast.ts",
    "content": "export type LiteralValueQuotes = \"'\" | \"\\\"\" | \"`\";\n\nexport interface Range {\n  range: [number, number];\n}\n\nexport interface Loc {\n  loc: {\n    end: {\n      column: number;\n      line: number;\n    };\n    start: {\n      column: number;\n      line: number;\n    };\n  };\n}\n\nexport interface MultilineMeta {\n  multilineQuotes?: LiteralValueQuotes[] | undefined;\n  supportsMultiline?: boolean | undefined;\n  surroundingBraces?: boolean | undefined;\n}\n\nexport interface WhitespaceMeta {\n  leadingWhitespace?: string | undefined;\n  trailingWhitespace?: string | undefined;\n}\n\nexport interface QuoteMeta {\n  closingQuote?: LiteralValueQuotes | undefined;\n  openingQuote?: LiteralValueQuotes | undefined;\n}\nexport interface BracesMeta {\n  closingBraces?: string | undefined;\n  openingBraces?: string | undefined;\n}\n\nexport interface CSSMeta {\n  leadingApply?: string | undefined;\n  trailingSemicolon?: string | undefined;\n}\n\nexport interface Indentation {\n  indentation: number;\n}\n\ninterface NodeBase extends Range, Loc {\n  [key: PropertyKey]: unknown;\n  type: string;\n}\n\ninterface LiteralBase extends NodeBase, MultilineMeta, QuoteMeta, BracesMeta, WhitespaceMeta, CSSMeta, Indentation, Range, Loc {\n  content: string;\n  raw: string;\n  attribute?: string | undefined;\n  isConcatenatedLeft?: boolean | undefined;\n  isConcatenatedRight?: boolean | undefined;\n  isInterpolated?: boolean | undefined;\n  priorLiterals?: Literal[] | undefined;\n}\n\nexport interface TemplateLiteral extends LiteralBase {\n  type: \"TemplateLiteral\";\n}\n\nexport interface StringLiteral extends LiteralBase {\n  type: \"StringLiteral\";\n}\n\nexport interface CSSClassListLiteral extends LiteralBase {\n  type: \"CSSClassListLiteral\";\n}\n\nexport type Literal = CSSClassListLiteral | StringLiteral | TemplateLiteral;\n"
  },
  {
    "path": "src/types/async.ts",
    "content": "export type Async<Fn extends (...args: any[]) => any> = (...params: Parameters<Fn>) => Promise<ReturnType<Fn>>;\n\nexport interface Warning<Options extends Record<string, any> = Record<string, any>> {\n  option: keyof Options & string;\n  title: string;\n  url?: string;\n}\n"
  },
  {
    "path": "src/types/estree.ts",
    "content": "import type { Rule } from \"eslint\";\n\n\ntype Nullable<Object extends object> = {\n  [Key in keyof Object]: Object[Key] | null;\n};\n\n\nexport type WithParent<BaseNode> = BaseNode & Nullable<Partial<Rule.NodeParentExtension>>;\n"
  },
  {
    "path": "src/types/rule.ts",
    "content": "import type { JSRuleDefinition } from \"eslint\";\nimport type { BaseIssue, BaseSchema, Default, InferOutput, OptionalSchema, StrictObjectSchema } from \"valibot\";\n\nimport type { CommonOptions } from \"better-tailwindcss:options/descriptions.js\";\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\n\n\nexport enum MatcherType {\n  /** Matches return values of anonymous functions via nested matchers. */\n  AnonymousFunctionReturn = \"anonymousFunctionReturn\",\n  /** Matches all object keys that are strings. */\n  ObjectKey = \"objectKeys\",\n  /** Matches all object values that are strings. */\n  ObjectValue = \"objectValues\",\n  /** Matches all strings  that are not matched by another matcher. */\n  String = \"strings\"\n}\n\nexport enum SelectorKind {\n  Attribute = \"attribute\",\n  Callee = \"callee\",\n  Tag = \"tag\",\n  Variable = \"variable\"\n}\n\nexport type Regex = string;\n\n/* Legacy matchers */\n\nexport type StringMatcher = {\n  match: MatcherType.String;\n};\n\nexport type ObjectKeyMatcher = {\n  match: MatcherType.ObjectKey;\n  pathPattern?: Regex | undefined;\n};\n\nexport type ObjectValueMatcher = {\n  match: MatcherType.ObjectValue;\n  pathPattern?: Regex | undefined;\n};\n\nexport const MATCHER_RESULT = {\n  MATCH: true,\n  NO_MATCH: false,\n  UNCROSSABLE_BOUNDARY: \"UNCROSSABLE_BOUNDARY\"\n} as const;\ntype MatcherFunctionResult = typeof MATCHER_RESULT[keyof typeof MATCHER_RESULT];\n\nexport type MatcherFunction = (node: unknown) => MatcherFunctionResult | MatcherFunctions;\nexport type MatcherFunctions = MatcherFunction[];\nexport type Matcher = ObjectKeyMatcher | ObjectValueMatcher | StringMatcher;\n\n/* New selector matchers */\n\nexport type SelectorStringMatcher = {\n  type: MatcherType.String;\n};\n\nexport type SelectorAnonymousFunctionReturnMatcher = {\n  match: (SelectorObjectKeyMatcher | SelectorObjectValueMatcher | SelectorStringMatcher)[];\n  type: MatcherType.AnonymousFunctionReturn;\n};\n\nexport type SelectorObjectKeyMatcher = {\n  type: MatcherType.ObjectKey;\n  path?: Regex | undefined;\n};\n\nexport type SelectorObjectValueMatcher = {\n  type: MatcherType.ObjectValue;\n  path?: Regex | undefined;\n};\n\nexport type SelectorMatcher =\n  | SelectorAnonymousFunctionReturnMatcher\n  | SelectorObjectKeyMatcher\n  | SelectorObjectValueMatcher\n  | SelectorStringMatcher;\n\ntype BaseSelector<Kind extends SelectorKind> = {\n  kind: Kind;\n  name: Regex;\n  match?: SelectorMatcher[] | undefined;\n};\n\nexport type Target = \"all\" | \"first\" | \"last\" | number;\nexport type CallTarget = Target;\nexport type ArgumentTarget = Target;\n\nexport type AttributeSelector = BaseSelector<SelectorKind.Attribute>;\n\nexport type CalleeSelector = {\n  kind: SelectorKind.Callee;\n  /** @deprecated Use targetCall instead. */\n  callTarget?: CallTarget | undefined;\n  match?: SelectorMatcher[] | undefined;\n  name?: Regex | undefined;\n  path?: Regex | undefined;\n  targetArgument?: ArgumentTarget | undefined;\n  targetCall?: CallTarget | undefined;\n};\n\nexport type TagSelector = {\n  kind: SelectorKind.Tag;\n  match?: SelectorMatcher[] | undefined;\n  name?: Regex | undefined;\n  path?: Regex | undefined;\n};\n\nexport type VariableSelector = BaseSelector<SelectorKind.Variable>;\n\nexport type Selector = AttributeSelector | CalleeSelector | TagSelector | VariableSelector;\nexport type Selectors = Selector[];\n\nexport type SelectorByKind<Kind extends SelectorKind> = Extract<Selector, { kind: Kind; }>;\n\nexport type Version = {\n  major: number;\n  minor: number;\n  patch: number;\n};\n\nexport type TailwindConfig = {\n  entryPoint?: string;\n  tailwindConfig?: string;\n};\n\nexport type TSConfig = {\n  tsconfig?: string;\n};\n\nexport type Schema = StrictObjectSchema<Record<string, OptionalSchema<BaseSchema<unknown, unknown, BaseIssue<unknown>>, Default<BaseSchema<unknown, unknown, BaseIssue<unknown>>, undefined>>>, undefined>;\nexport type JsonSchema<RawSchema extends Schema> = InferOutput<RawSchema>;\n\nexport type RuleCategory = \"correctness\" | \"stylistic\";\n\nexport interface CreateRuleOptions<\n  Name extends string,\n  Messages extends Record<string, string>,\n  OptionsSchema extends Schema = Schema,\n  Options extends Record<string, any> = CommonOptions & JsonSchema<OptionsSchema>,\n  Category extends RuleCategory = RuleCategory,\n  Recommended extends boolean = boolean\n> {\n  /** Whether the rule should automatically fix problems. */\n  autofix: boolean;\n  /** The category of the rule. */\n  category: Category;\n  /** A brief description of the rule. */\n  description: string;\n  /** The URL to the rule documentation. */\n  docs: string;\n  /** Lint the literals in the given context. */\n  lintLiterals: (ctx: RuleContext<Messages, Options>, literals: Literal[]) => void;\n  /** The name of the rule. */\n  name: Name;\n  /** Whether the rule is enabled in the recommended configs. */\n  recommended: Recommended;\n  initialize?: (ctx: RuleContext<Messages, Options>) => void;\n  /** The messages for the rule. */\n  messages?: Messages;\n  /** The schema for the rule options. */\n  schema?: OptionsSchema;\n}\n\nexport interface ESLintRule<\n  Name extends string = string,\n  Messages extends Record<string, string> = Record<string, string>,\n  Options extends Record<string, any> = Record<string, any>,\n  Category extends RuleCategory = RuleCategory,\n  Recommended extends boolean = boolean\n> {\n  category: Category;\n  messages: Messages | undefined;\n  name: Name;\n  get options(): Options;\n  recommended: Recommended;\n  rule: JSRuleDefinition<{\n    MessageIds: keyof Messages & string;\n    RuleOptions: [Required<Options>];\n  }>;\n}\n\nexport interface RuleContext<\n  Messages extends Record<string, string> | undefined,\n  Options extends Record<string, any>\n> {\n  cwd: string;\n  docs: string;\n  /** The installation path of Tailwind CSS. */\n  installation: string;\n  options: Options;\n  report: <\n    const MsgId extends MessageId<Messages>\n  >(info:\n    (\n      | (\n        MsgId extends string\n          ? Messages extends Record<string, string>\n            ? MsgId extends keyof Messages\n              ? {\n                data: Record<ExtractVariables<Messages[MsgId]>, string> extends infer Data\n                  ? keyof Data extends never\n                    ? never\n                    : Data\n                  : never;\n                id: MsgId;\n                fix?: string;\n                warnings?: (Warning | undefined)[] | undefined;\n              }\n              : never\n            : never\n          : never\n        )\n      | {\n        fix?: string;\n        message?: string;\n        warnings?: (Warning<Options> | undefined)[] | undefined;\n      }\n    ) & {\n      range: [number, number];\n    }\n\n  ) => void;\n  /** The Tailwind CSS Version. */\n  version: Version;\n}\n\nexport type Context<Rule extends ESLintRule = ESLintRule> = RuleContext<Rule[\"messages\"], Rule[\"options\"]>;\n\nexport type MessageId<Messages extends Record<string, any> | undefined> = Messages extends Record<string, any>\n  ? keyof Messages\n  : never;\n\ntype Trim<Content extends string> =\n  Content extends ` ${infer Rest}` ? Trim<Rest>\n    : Content extends `${infer Rest} ` ? Trim<Rest>\n      : Content;\n\nexport type ExtractVariables<Template extends string> =\n  Template extends `${string}{{${infer RawVariable}}}${infer Rest}`\n    ? ExtractVariables<Rest> | Trim<RawVariable>\n    : never;\n"
  },
  {
    "path": "src/utils/ast.ts",
    "content": "import type { Rule } from \"eslint\";\nimport type { SourceLocation } from \"estree\";\n\n\nexport function getLocByRange(ctx: Rule.RuleContext, range: [number, number]): SourceLocation {\n  const [rangeStart, rangeEnd] = range;\n\n  const loc: SourceLocation = {\n    end: ctx.sourceCode.getLocFromIndex(rangeEnd),\n    start: ctx.sourceCode.getLocFromIndex(rangeStart)\n  };\n\n  return loc;\n}\n"
  },
  {
    "path": "src/utils/class.ts",
    "content": "import type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\ninterface ClassParts {\n  base: string;\n  important: [boolean, boolean];\n  negative: boolean;\n  prefix: string;\n  separator: string;\n  variants: string[] | undefined;\n}\n\nexport function buildClass(ctx: Context, { base, important, negative, prefix, separator, variants }: ClassParts): string {\n\n  const importantAtStart = important[0] && \"!\";\n  const importantAtEnd = important[1] && \"!\";\n  const negativePrefix = negative && \"-\";\n\n  if(ctx.version.major >= 4){\n    return [\n      prefix,\n      ...variants ?? [],\n      [importantAtStart, negativePrefix, base, importantAtEnd].filter(Boolean).join(\"\")\n    ].filter(Boolean).join(separator);\n  } else {\n    return [\n      ...variants ?? [],\n      [importantAtStart, prefix, negativePrefix, base, importantAtEnd].filter(Boolean).join(\"\")\n    ].filter(Boolean).join(separator);\n  }\n}\n"
  },
  {
    "path": "src/utils/context.ts",
    "content": "import { resolve } from \"node:path\";\n\nimport { withCache } from \"better-tailwindcss:utils/cache.js\";\nimport { findPathRecursive } from \"better-tailwindcss:utils/fs.js\";\nimport { resolveCss } from \"better-tailwindcss:utils/resolvers.js\";\n\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context, Version } from \"better-tailwindcss:types/rule.js\";\n\n\nexport interface AsyncContext {\n  cwd: string;\n  installation: string;\n  tailwindConfigPath: string;\n  tsconfigPath: string | undefined;\n  version: Version;\n  warnings: (Warning | undefined)[];\n}\n\nexport function async(ctx: Context): AsyncContext {\n  const { path: resolvedTailwindPath, warnings: tailwindConfigWarnings } = getTailwindConfigPath({ configPath: ctx.options.entryPoint ?? ctx.options.tailwindConfig, cwd: ctx.cwd, version: ctx.version });\n  const { path: resolvedTSConfigPath, warnings: tsconfigWarnings } = getTSConfigPath({ configPath: ctx.options.tsconfig, cwd: ctx.cwd });\n\n  return {\n    cwd: ctx.cwd,\n    installation: ctx.installation,\n    tailwindConfigPath: resolvedTailwindPath,\n    tsconfigPath: resolvedTSConfigPath,\n    version: ctx.version,\n    warnings: [...tailwindConfigWarnings, ...tsconfigWarnings]\n  };\n}\n\nfunction getTSConfigPath({ configPath, cwd }: {\n  configPath: string | undefined;\n  cwd: string;\n}) {\n  return withCache(\"tsconfig-path\", configPath, () => {\n    const potentialPaths = [\n      ...configPath ? [configPath] : [],\n      \"tsconfig.json\",\n      \"jsconfig.json\"\n    ];\n\n    const foundConfigPath = findPathRecursive(cwd, cwd, potentialPaths);\n    const warning = getConfigPathWarning(configPath, foundConfigPath);\n\n    return {\n      path: foundConfigPath,\n      warnings: [warning]\n    };\n  });\n}\n\nexport function getTailwindConfigPath({ configPath, cwd, version }: {\n  configPath: string | undefined;\n  cwd: string;\n  version: Version;\n}) {\n  return withCache(\"config-path\", configPath, () => {\n    if(version.major >= 4){\n\n      const foundConfigPath = configPath && findPathRecursive(cwd, cwd, [configPath]);\n      const warning = getEntryPointWarning(configPath, foundConfigPath);\n\n      if(foundConfigPath){\n        return {\n          path: foundConfigPath,\n          warnings: [warning]\n        };\n      }\n\n      const defaultConfigPath = resolveCss(\"tailwindcss/theme.css\", cwd);\n\n      if(!defaultConfigPath){\n        throw new Error(\"No default tailwind config found. Please ensure you have Tailwind CSS installed.\");\n      }\n\n      return {\n        path: defaultConfigPath,\n        warnings: [warning]\n      };\n    }\n\n    if(version.major <= 3){\n      const defaultPaths = [\n        \"tailwind.config.js\",\n        \"tailwind.config.cjs\",\n        \"tailwind.config.mjs\",\n        \"tailwind.config.ts\"\n      ];\n\n      const foundConfigPath = configPath && findPathRecursive(cwd, cwd, [configPath]);\n\n      const foundDefaultPath = findPathRecursive(cwd, cwd, defaultPaths);\n      const warning = getConfigPathWarning(configPath, foundConfigPath);\n\n      return {\n        path: foundConfigPath ?? foundDefaultPath ?? \"default\",\n        warnings: [warning]\n      };\n    }\n\n    throw new Error(`Unsupported Tailwind CSS version: ${version.major}. Please use a version between 3 and 4.`);\n  });\n}\n\nfunction getEntryPointWarning(entryPoint: string | undefined, foundEntryPoint: string | undefined): Warning | undefined {\n  if(!!entryPoint && !!foundEntryPoint){\n    return;\n  }\n\n  return {\n    option: \"entryPoint\",\n    title: `No tailwind css entry point found at \\`${entryPoint}\\``\n  };\n}\n\n\nfunction getConfigPathWarning(configPath: string | undefined, foundConfigPath: string | undefined): Warning | undefined {\n  if(!configPath){\n    return;\n  }\n\n  if(foundConfigPath && resolve(configPath) === resolve(foundConfigPath)){\n    return;\n  }\n\n  return {\n    option: \"tsconfig\",\n    title: `No tsconfig found at \\`${configPath}\\``\n  };\n}\n"
  },
  {
    "path": "src/utils/lint.ts",
    "content": "import { splitClasses, splitWhitespaces } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\ntype RemoveNeverProperties<ObjectType extends Record<string, any>> = {\n  [Key in keyof ObjectType as ObjectType[Key] extends never ? never : Key]: ObjectType[Key]\n};\n\nexport function lintClasses<\n  const Ctx extends Context\n>(\n  ctx: Ctx,\n  literal: Literal,\n  report: (className: string, index: number, after: string[]) =>\n      | ((\n        Parameters<Ctx[\"report\"]>[0] extends infer DataAndId\n          ? (\n            DataAndId extends Record<\"data\" | \"id\", any>\n              ? {\n                data: DataAndId[\"data\"];\n                id: DataAndId[\"id\"];\n                fix?: string;\n                message?: undefined;\n                warnings?: (Warning | undefined)[];\n              }\n              : never\n          )\n          : never\n      ) extends infer Result extends Record<string, any>\n        ? {\n          [Key in keyof Result as Result[Key] extends never ? never : Key]: Result[Key]\n        }\n        : never\n      )\n      | false\n      | undefined\n      | {\n        message: string;\n        fix?: string;\n        warnings?: (Warning | undefined)[];\n      }\n): void {\n\n  const classChunks = splitClasses(literal.content);\n  const whitespaceChunks = splitWhitespaces(literal.content);\n\n  const startsWithWhitespace = whitespaceChunks.length > 0 && whitespaceChunks[0] !== \"\";\n\n  const after = [...classChunks];\n\n  for(let classIndex = 0, stringIndex = 0; classIndex < classChunks.length; classIndex++){\n\n    const className = classChunks[classIndex];\n\n    if(startsWithWhitespace){\n      stringIndex += whitespaceChunks[classIndex].length;\n    }\n\n    const startIndex = stringIndex;\n    const endIndex = stringIndex + className.length;\n\n    stringIndex = endIndex;\n\n    if(!startsWithWhitespace){\n      stringIndex += whitespaceChunks[classIndex + 1].length;\n    }\n\n    const result = report(className, classIndex, after);\n\n    if(result === undefined || result === false){\n      continue;\n    }\n\n    const [literalStart] = literal.range;\n\n    if(typeof result === \"object\" && result.fix !== undefined){\n      after[classIndex] = result.fix;\n    }\n\n    ctx.report({\n      message: `Expected ${className} to be ${result.fix ?? \"\"}.`,\n      range: [\n        literalStart + startIndex + (literal.openingQuote?.length ?? 0) + (literal.closingBraces?.length ?? 0),\n        literalStart + endIndex + (literal.openingQuote?.length ?? 0) + (literal.closingBraces?.length ?? 0)\n      ],\n      ...\"warnings\" in result && { warnings: result.warnings },\n      ...\"data\" in result && { data: result.data },\n      ...\"message\" in result && { id: undefined, message: result.message },\n      ...\"id\" in result && { id: result.id, message: undefined },\n      ...typeof result === \"object\" && result.fix !== undefined && { fix: result.fix }\n    });\n\n  }\n\n}\n"
  },
  {
    "path": "src/utils/matchers.test.ts",
    "content": "import { parse } from \"espree\";\nimport { assert, describe, expect, it } from \"vitest\";\n\nimport {\n  getESObjectPath,\n  hasESNodeParentExtension,\n  isESNode,\n  isESObjectKey,\n  isESStringLike,\n  isInsideObjectValue\n} from \"better-tailwindcss:parsers/es.js\";\nimport { noUnnecessaryWhitespace } from \"better-tailwindcss:rules/no-unnecessary-whitespace.js\";\nimport { findNode, lint, withParentNodeExtension } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { MatcherType } from \"better-tailwindcss:types/rule.js\";\n\nimport type { Node as ESNode } from \"estree\";\n\n\ndescribe(\"matchers\", () => {\n  describe(\"getObjectPath\", () => {\n\n    {\n\n      const code = `const obj = {\n        root: {\n          nested: {\n            value: \"value\"\n          }\n        }\n      };`;\n\n      it(\"should return the correct object path for keys\", () => {\n\n        const ast = withParentNodeExtension(parse(code, { ecmaVersion: \"latest\" }) as ESNode);\n\n        const root = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Identifier\" && node.name === \"root\";\n        });\n\n        const nested = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Identifier\" && node.name === \"nested\";\n        });\n\n        const value = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Identifier\" && node.name === \"value\";\n        });\n\n        assert(root);\n        assert(nested);\n        assert(value);\n\n        expect(getESObjectPath(root)).toBe(\"root\");\n        expect(getESObjectPath(nested)).toBe(\"root.nested\");\n        expect(getESObjectPath(value)).toBe(\"root.nested.value\");\n\n      });\n\n      it(\"should return the correct object path for values\", () => {\n\n        const ast = withParentNodeExtension(parse(code, { ecmaVersion: \"latest\" }) as ESNode);\n\n        const value = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && isESStringLike(node) && isInsideObjectValue(node);\n        });\n\n        assert(value);\n\n        const path = getESObjectPath(value);\n        expect(path).toBe(\"root.nested.value\");\n\n      });\n\n      it(\"should return the correct object path for template literal values\", () => {\n\n        const code = `const obj = {\n          root: {\n            nested: {\n              value: \\`value\\`\n            }\n          }\n        };`;\n\n        const ast = withParentNodeExtension(parse(code, { ecmaVersion: \"latest\" }) as ESNode);\n\n        const value = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && isESStringLike(node) && isInsideObjectValue(node);\n        });\n\n        assert(value);\n\n        const path = getESObjectPath(value);\n        expect(path).toBe(\"root.nested.value\");\n\n      });\n\n      it(\"should put names in quotes if they are not valid identifiers\", () => {\n\n        const code = `const obj = {\n            \"root-key\": {\n              \"1nested\": {\n                \"deeply_nested_value\": \"value\"\n              }\n            }\n          };`;\n\n        const ast = withParentNodeExtension(parse(code, { ecmaVersion: \"latest\" }) as ESNode);\n\n        const root = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Literal\" && node.value === \"root-key\";\n        });\n\n        const nested = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Literal\" && node.value === \"1nested\";\n        });\n\n        const value = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Literal\" && node.value === \"deeply_nested_value\";\n        });\n\n        assert(root);\n        assert(nested);\n        assert(value);\n\n        expect(getESObjectPath(root)).toBe(`[\"root-key\"]`);\n        expect(getESObjectPath(nested)).toBe(`[\"root-key\"][\"1nested\"]`);\n        expect(getESObjectPath(value)).toBe(`[\"root-key\"][\"1nested\"].deeply_nested_value`);\n\n      });\n    }\n\n    {\n\n      const code = `const obj = {\n        root: [\n          {\n            value1: \"value1\"\n          },\n          {\n            value2: \"value2\"\n          }\n        ]\n      };`;\n\n      it(\"should return the correct index in arrays\", () => {\n\n        const ast = withParentNodeExtension(parse(code, { ecmaVersion: \"latest\" }) as ESNode);\n\n        const value1 = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Identifier\" && node.name === \"value1\";\n        });\n\n        const value2 = findNode(ast, (node): node is ESNode => {\n          return isESNode(node) && hasESNodeParentExtension(node) && isESObjectKey(node) && node.type === \"Identifier\" && node.name === \"value2\";\n        });\n\n        assert(value1);\n        assert(value2);\n\n        expect(getESObjectPath(value1)).toBe(\"root[0].value1\");\n        expect(getESObjectPath(value2)).toBe(\"root[1].value2\");\n\n      });\n\n    }\n\n  });\n\n  describe(\"callees\", () => {\n\n    it(\"should lint literals in call expressions\", () => {\n\n      const dirtyDefined = `defined({\n        \"nested\": {\n          \"matched\": \"  b  a  \",\n        },\n        \"deeply\": {\n          \"nested\": {\n            \"unmatched\": \"  b  a  \",\n            \"matched\": \"  b  a  \"\n          },\n        },\n        \"multiline\": {\n          \"matched\": \\`\n            d  a\n            b  c\n          \\`\n        }\n      });`;\n\n      const cleanDefined = `defined({\n        \"nested\": {\n          \"matched\": \"b a\",\n        },\n        \"deeply\": {\n          \"nested\": {\n            \"unmatched\": \"  b  a  \",\n            \"matched\": \"b a\"\n          },\n        },\n        \"multiline\": {\n          \"matched\": \\`\n            d a\n            b c\n          \\`\n        }\n      });`;\n\n      lint(\n        noUnnecessaryWhitespace,\n\n        {\n          invalid: [\n            {\n              jsx: dirtyDefined,\n              jsxOutput: cleanDefined,\n              svelte: `<script>${dirtyDefined}</script>`,\n              svelteOutput: `<script>${cleanDefined}</script>`,\n              vue: `<script>${dirtyDefined}</script>`,\n              vueOutput: `<script>${cleanDefined}</script>`,\n\n              errors: 8,\n              options: [{\n                callees: [\n                  [\n                    \"defined\",\n                    [\n                      {\n                        match: MatcherType.ObjectValue,\n                        pathPattern: \"^matched|\\\\.matched\"\n                      }\n                    ]\n                  ]\n                ]\n              }]\n            }\n          ]\n        }\n      );\n\n    });\n\n    it(\"should match callees names via regex\", () => {\n      lint(noUnnecessaryWhitespace, {\n        invalid: [\n          {\n            jsx: `testStyles(\" lint \");`,\n            jsxOutput: `testStyles(\"lint\");`,\n            svelte: `<script>testStyles(\" lint \");</script>`,\n            svelteOutput: `<script>testStyles(\"lint\");</script>`,\n            vue: `<script>testStyles(\" lint \");</script>`,\n            vueOutput: `<script>testStyles(\"lint\");</script>`,\n\n            errors: 2,\n            options: [{\n              callees: [[\"^.*Styles$\", [{ match: MatcherType.String }]]]\n            }]\n          }\n        ]\n      });\n    });\n\n    it(\"should match variable names via regex\", () => {\n      lint(noUnnecessaryWhitespace, {\n        invalid: [\n          {\n            jsx: `const testStyles = \" lint \";`,\n            jsxOutput: `const testStyles = \"lint\";`,\n            svelte: `<script>const testStyles = \" lint \";</script>`,\n            svelteOutput: `<script>const testStyles = \"lint\";</script>`,\n            vue: `<script>const testStyles = \" lint \";</script>`,\n            vueOutput: `<script>const testStyles = \"lint\";</script>`,\n\n            errors: 2,\n            options: [{\n              variables: [[\"^.*Styles$\", [{ match: MatcherType.String }]]]\n            }]\n          }\n        ]\n      });\n    });\n\n    it(\"should match attributes via regex\", () => {\n      lint(noUnnecessaryWhitespace, {\n        invalid: [\n          {\n            jsx: `<img testStyles=\" lint \" />`,\n            jsxOutput: `<img testStyles=\"lint\" />`,\n            svelte: `<img testStyles=\" lint \" />`,\n            svelteOutput: `<img testStyles=\"lint\" />`,\n            vue: `<template><img testStyles=\" lint \" /> </template>`,\n            vueOutput: `<template><img testStyles=\"lint\" /> </template>`,\n\n            errors: 2,\n            options: [{\n              attributes: [[\"^.*Styles$\", [{ match: MatcherType.String }]]]\n            }]\n          }\n        ]\n      });\n    });\n\n  });\n\n  describe(\"variables\", () => {\n\n    it(\"should lint literals in variables\", () => {\n\n      const dirtyDefined = `const defined = {\n        \"nested\": {\n          \"matched\": \"  b  a  \",\n        },\n        \"deeply\": {\n          \"nested\": {\n            \"unmatched\": \"  b  a  \",\n            \"matched\": \"  b  a  \"\n          },\n        },\n        \"multiline\": {\n          \"matched\": \\`\n            d  a\n            b  c\n          \\`\n        }\n      };`;\n\n      const cleanDefined = `const defined = {\n        \"nested\": {\n          \"matched\": \"b a\",\n        },\n        \"deeply\": {\n          \"nested\": {\n            \"unmatched\": \"  b  a  \",\n            \"matched\": \"b a\"\n          },\n        },\n        \"multiline\": {\n          \"matched\": \\`\n            d a\n            b c\n          \\`\n        }\n      };`;\n\n      lint(\n        noUnnecessaryWhitespace,\n\n        {\n          invalid: [\n            {\n              jsx: dirtyDefined,\n              jsxOutput: cleanDefined,\n              svelte: `<script>${dirtyDefined}</script>`,\n              svelteOutput: `<script>${cleanDefined}</script>`,\n              vue: `<script>${dirtyDefined}</script>`,\n              vueOutput: `<script>${cleanDefined}</script>`,\n\n              errors: 8,\n              options: [{\n                variables: [\n                  [\n                    \"defined\",\n                    [\n                      {\n                        match: MatcherType.ObjectValue,\n                        pathPattern: \"^matched|\\\\.matched\"\n                      }\n                    ]\n                  ]\n                ]\n              }]\n            }\n          ]\n        }\n      );\n\n    });\n\n    it(\"should not cross arrow function boundaries\", () => {\n      lint(noUnnecessaryWhitespace, {\n        valid: [\n          {\n            jsx: `const defined = () => \" b a \";`,\n            svelte: `<script>const defined = () => \" b a \";</script>`,\n            vue: `<script>const defined = () => \" b a \";</script>`,\n\n            options: [{\n              variables: [[\"defined\", [{ match: MatcherType.String }]]]\n            }]\n          }\n        ]\n      });\n    });\n  });\n\n  describe(\"attributes\", () => {\n\n    it(\"should lint literals in attributes\", () => {\n\n      const dirtyDefined = `{\n        \"nested\": {\n          \"matched\": \"  b  a  \",\n        },\n        \"deeply\": {\n          \"nested\": {\n            \"unmatched\": \"  b  a  \",\n            \"matched\": \"  b  a  \"\n          },\n        },\n        \"multiline\": {\n          \"matched\": \\`\n            d  a\n            b  c\n          \\`\n        }\n      }`;\n\n      const cleanDefined = `{\n        \"nested\": {\n          \"matched\": \"b a\",\n        },\n        \"deeply\": {\n          \"nested\": {\n            \"unmatched\": \"  b  a  \",\n            \"matched\": \"b a\"\n          },\n        },\n        \"multiline\": {\n          \"matched\": \\`\n            d a\n            b c\n          \\`\n        }\n      }`;\n\n      lint(\n        noUnnecessaryWhitespace,\n\n        {\n          invalid: [\n            {\n              jsx: `<img defined={${dirtyDefined}} />`,\n              jsxOutput: `<img defined={${cleanDefined}} />`,\n              svelte: `<img defined={${dirtyDefined}} />`,\n              svelteOutput: `<img defined={${cleanDefined}} />`,\n\n              errors: 8,\n              options: [{\n                attributes: [\n                  [\n                    \"defined\",\n                    [\n                      {\n                        match: MatcherType.ObjectValue,\n                        pathPattern: \"^matched|\\\\.matched\"\n                      }\n                    ]\n                  ]\n                ]\n              }]\n            }\n          ]\n        }\n      );\n\n    });\n\n  });\n\n  describe(\"template literal tags\", () => {\n\n    it(\"should lint class names in tagged template literals when matched using the strings matcher\", () => {\n      lint(\n        noUnnecessaryWhitespace,\n\n        {\n          invalid: [\n            {\n              jsx: \"defined`  lint  lint  `\",\n              jsxOutput: \"defined`lint lint`\",\n              svelte: \"<script>defined`  lint  lint  `</script>\",\n              svelteOutput: \"<script>defined`lint lint`</script>\",\n              vue: \"defined`  lint  lint  `\",\n              vueOutput: \"defined`lint lint`\",\n\n              errors: 3,\n              options: [{ tags: [[\"defined\", [{ match: MatcherType.String }]]] }]\n            }\n          ]\n        }\n      );\n    });\n\n    it(\"should lint class names in nested literal expressions inside tagged template literals when matched using the strings matcher\", () => {\n      lint(\n        noUnnecessaryWhitespace,\n\n        {\n          invalid: [\n            {\n              jsx: \"defined` lint ${\\\"  lint  lint  \\\"} lint `\",\n              jsxOutput: \"defined`lint ${\\\"lint lint\\\"} lint`\",\n              svelte: \"<script>defined` lint ${\\\"  lint  lint  \\\"} lint `</script>\",\n              svelteOutput: \"<script>defined`lint ${\\\"lint lint\\\"} lint`</script>\",\n              vue: \"defined` lint ${\\\"  lint  lint  \\\"} lint `\",\n              vueOutput: \"defined`lint ${\\\"lint lint\\\"} lint`\",\n\n              errors: 5,\n              options: [{ tags: [[\"defined\", [{ match: MatcherType.String }]]] }]\n            }\n          ],\n          valid: [\n            {\n              jsx: \"notDefined` lint ${\\\"  lint  lint  \\\"} lint `\",\n              svelte: \"<script>notDefined` lint ${\\\"  lint  lint  \\\"} lint `</script>\",\n              vue: \"notDefined` lint ${\\\"  lint  lint  \\\"} lint `\",\n\n              options: [{ tags: [[\"defined\", [{ match: MatcherType.String }]]] }]\n            }\n          ]\n        }\n      );\n      lint(\n        noUnnecessaryWhitespace,\n\n        {\n          invalid: [\n            {\n              jsx: \"defined`lint ${\\\"  lint  lint  \\\"} lint`\",\n              jsxOutput: \"defined`lint ${\\\"lint lint\\\"} lint`\",\n              svelte: \"<script>defined`lint ${\\\"  lint  lint  \\\"} lint`</script>\",\n              svelteOutput: \"<script>defined`lint ${\\\"lint lint\\\"} lint`</script>\",\n              vue: \"defined`lint ${\\\"  lint  lint  \\\"} lint`\",\n              vueOutput: \"defined`lint ${\\\"lint lint\\\"} lint`\",\n\n              errors: 3,\n              options: [{ tags: [[\"defined\", [{ match: MatcherType.String }]]] }]\n            }\n          ],\n          valid: [\n            {\n              jsx: \"notDefined`lint ${\\\"  lint  lint  \\\"} lint`\",\n              svelte: \"<script>notDefined`lint ${\\\"  lint  lint  \\\"} lint`</script>\",\n              vue: \"notDefined`lint ${\\\"  lint  lint  \\\"} lint`\",\n\n              options: [{ tags: [[\"defined\", [{ match: MatcherType.String }]]] }]\n            }\n          ]\n        }\n      );\n    });\n\n  });\n\n  it(\"should lint literals inside object keys when matched\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"defined({ \\\" lint \\\": \\\" ignore \\\" })\",\n          jsxOutput: \"defined({ \\\"lint\\\": \\\" ignore \\\" })\",\n          svelte: \"<script>defined({ \\\" lint \\\": \\\" ignore \\\" })</script>\",\n          svelteOutput: \"<script>defined({ \\\"lint\\\": \\\" ignore \\\" })</script>\",\n          vue: \"defined({ \\\" lint \\\": \\\" ignore \\\" })\",\n          vueOutput: \"defined({ \\\"lint\\\": \\\" ignore \\\" })\",\n\n          errors: 2,\n          options: [{\n            callees: [[\"defined\", [{ match: MatcherType.ObjectKey }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint literals inside object values when matched\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"defined({ \\\" ignore \\\": \\\" lint \\\" })\",\n          jsxOutput: \"defined({ \\\" ignore \\\": \\\"lint\\\" })\",\n          svelte: \"<script>defined({ \\\" ignore \\\": \\\" lint \\\" })</script>\",\n          svelteOutput: \"<script>defined({ \\\" ignore \\\": \\\"lint\\\" })</script>\",\n          vue: \"defined({ \\\" ignore \\\": \\\" lint \\\" })\",\n          vueOutput: \"defined({ \\\" ignore \\\": \\\"lint\\\" })\",\n\n          errors: 2,\n          options: [{\n            callees: [[\"defined\", [{ match: MatcherType.ObjectValue }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint only strings not matched by other matchers\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"defined(\\\" lint \\\", { \\\" ignore \\\": \\\" ignore \\\" }, [\\\" lint \\\"])\",\n          jsxOutput: \"defined(\\\"lint\\\", { \\\" ignore \\\": \\\" ignore \\\" }, [\\\"lint\\\"])\",\n          svelte: \"<script>defined(\\\" lint \\\", { \\\" ignore \\\": \\\" ignore \\\" }, [\\\" lint \\\"])</script>\",\n          svelteOutput: \"<script>defined(\\\"lint\\\", { \\\" ignore \\\": \\\" ignore \\\" }, [\\\"lint\\\"])</script>\",\n          vue: \"defined(\\\" lint \\\", { \\\" ignore \\\": \\\" ignore \\\" }, [\\\" lint \\\"])\",\n          vueOutput: \"defined(\\\"lint\\\", { \\\" ignore \\\": \\\" ignore \\\" }, [\\\"lint\\\"])\",\n\n          errors: 4,\n          options: [{\n            callees: [[\"defined\", [{ match: MatcherType.String }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should lint strings inside template literal expressions when matched using the strings matcher\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"defined(` lint ${\\\" lint \\\"} lint `)\",\n          jsxOutput: \"defined(`lint ${\\\"lint\\\"} lint`)\",\n          svelte: \"<script>defined(` lint ${\\\" lint \\\"} lint `)</script>\",\n          svelteOutput: \"<script>defined(`lint ${\\\"lint\\\"} lint`)</script>\",\n          vue: \"defined(` lint ${\\\" lint \\\"} lint `)\",\n          vueOutput: \"defined(`lint ${\\\"lint\\\"} lint`)\",\n\n          errors: 4,\n          options: [{\n            callees: [[\"defined\", [{ match: MatcherType.String }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should not double report if multiple matchers match the same literal\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"defined({ \\\" lint \\\": \\\" lint \\\" })\",\n          jsxOutput: \"defined({ \\\"lint\\\": \\\"lint\\\" })\",\n\n          errors: 4,\n          options: [{\n            callees: [[\"defined\", [{ match: MatcherType.ObjectKey }, { match: MatcherType.ObjectValue }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n  it(\"should still handle callees even when they are object values\", () => {\n    lint(noUnnecessaryWhitespace, {\n      invalid: [\n        {\n          jsx: \"<img class={{ key: defined('  a b c  ')}} />\",\n          jsxOutput: \"<img class={{ key: defined('a b c')}} />\",\n\n          errors: 2,\n          options: [{\n            attributes: [[\"class\", [{ match: MatcherType.ObjectValue }]]],\n            callees: [[\"defined\", [{ match: MatcherType.String }]]]\n          }]\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "src/utils/matchers.ts",
    "content": "import { hasESNodeParentExtension, isESSimpleStringLiteral } from \"better-tailwindcss:parsers/es.js\";\nimport { MATCHER_RESULT } from \"better-tailwindcss:types/rule.js\";\nimport { getCachedRegex } from \"better-tailwindcss:utils/regex.js\";\nimport { isGenericNodeWithParent } from \"better-tailwindcss:utils/utils.js\";\n\nimport type { Rule } from \"eslint\";\nimport type { Node as ESNode } from \"estree\";\n\nimport type { AttributeMatchers, AttributeName, Attributes } from \"better-tailwindcss:options/schemas/attributes.js\";\nimport type { CalleeMatchers, CalleeName, Callees } from \"better-tailwindcss:options/schemas/callees.js\";\nimport type { TagMatchers, TagName, Tags } from \"better-tailwindcss:options/schemas/tags.js\";\nimport type { VariableMatchers, VariableName, Variables } from \"better-tailwindcss:options/schemas/variables.js\";\nimport type { WithParent } from \"better-tailwindcss:types/estree.js\";\nimport type { MatcherFunctions } from \"better-tailwindcss:types/rule.js\";\nimport type { GenericNodeWithParent } from \"better-tailwindcss:utils/utils.js\";\n\n\nexport function getLiteralNodesByMatchers<Node>(ctx: Rule.RuleContext, node: unknown, matcherFunctions: MatcherFunctions): Node[] {\n  if(!isGenericNodeWithParent(node)){ return []; }\n\n  const nestedLiterals = findMatchingNestedNodes<Node>(node, matcherFunctions);\n\n  const self = matcherFunctions.reduce<Node[]>((self, matcherFunction) => {\n    const result = matcherFunction(node);\n\n    if(result === MATCHER_RESULT.NO_MATCH){\n      return self;\n    } else if(result === MATCHER_RESULT.MATCH){\n      return [node as Node, ...self];\n    } else if(result === MATCHER_RESULT.UNCROSSABLE_BOUNDARY){\n      return self;\n    } else if(Array.isArray(result)){\n      self.push(...findMatchingNestedNodes<Node>(node, result));\n    }\n    return self;\n  }, []);\n\n  return [...nestedLiterals, ...self];\n}\n\nfunction findMatchingNestedNodes<Node>(node: GenericNodeWithParent, matcherFunctions: MatcherFunctions) {\n  return Object.entries(node).reduce<Node[]>((matchedNodes, [key, value]) => {\n    if(!value || typeof value !== \"object\" || key === \"parent\"){\n      return matchedNodes;\n    }\n\n    let isMatch = false;\n    let currentMatcherFunctions: MatcherFunctions = [...matcherFunctions];\n    let hasMatcherFunctions = currentMatcherFunctions.length > 0;\n\n    while(hasMatcherFunctions){\n      const nextMatcherFunctions: MatcherFunctions = [];\n      const nestedMatcherFunctions: MatcherFunctions = [];\n\n      for(const matcherFunction of currentMatcherFunctions){\n        const result = matcherFunction(value);\n\n        if(result === MATCHER_RESULT.NO_MATCH){\n          nextMatcherFunctions.push(matcherFunction);\n          continue;\n        }\n\n        if(result === MATCHER_RESULT.MATCH){\n          isMatch = true;\n          nextMatcherFunctions.push(matcherFunction);\n          continue;\n        }\n\n        if(result === MATCHER_RESULT.UNCROSSABLE_BOUNDARY){\n          continue;\n        }\n\n        for(const nestedMatcherFunction of result){\n          nestedMatcherFunctions.push(nestedMatcherFunction);\n        }\n      }\n\n      hasMatcherFunctions = nestedMatcherFunctions.length > 0;\n      currentMatcherFunctions = hasMatcherFunctions\n        ? nestedMatcherFunctions\n        : nextMatcherFunctions;\n    }\n\n    if(isMatch){\n      matchedNodes.push(value as Node);\n    }\n\n    if(currentMatcherFunctions.length === 0){\n      return matchedNodes;\n    }\n\n    matchedNodes.push(...findMatchingNestedNodes<Node>(value, currentMatcherFunctions));\n    return matchedNodes;\n  }, []);\n}\n\nexport function matchesPathPattern(path: string, pattern: string): boolean {\n  return getCachedRegex(pattern).test(path);\n}\n\nexport function isCalleeName(callee: Callees[number]): callee is CalleeName {\n  return typeof callee === \"string\";\n}\n\nexport function isCalleeMatchers(callee: Callees[number]): callee is CalleeMatchers {\n  return Array.isArray(callee) && typeof callee[0] === \"string\" && Array.isArray(callee[1]);\n}\n\nexport function isVariableName(variable: Variables[number]): variable is VariableName {\n  return typeof variable === \"string\";\n}\n\nexport function isVariableMatchers(variable: Variables[number]): variable is VariableMatchers {\n  return Array.isArray(variable) && typeof variable[0] === \"string\" && Array.isArray(variable[1]);\n}\n\nexport function isTagName(tag: Tags[number]): tag is TagName {\n  return typeof tag === \"string\";\n}\n\nexport function isTagMatchers(tag: Tags[number]): tag is TagMatchers {\n  return Array.isArray(tag) && typeof tag[0] === \"string\" && Array.isArray(tag[1]);\n}\n\nexport function isAttributesName(attributes: Attributes[number]): attributes is AttributeName {\n  return typeof attributes === \"string\";\n}\n\nexport function isAttributesMatchers(attributes: Attributes[number]): attributes is AttributeMatchers {\n  return Array.isArray(attributes) && typeof attributes[0] === \"string\" && Array.isArray(attributes[1]);\n}\n\nexport function isInsideConditionalExpressionTest(node: WithParent<ESNode>): boolean {\n  if(!hasESNodeParentExtension(node)){ return false; }\n  if(node.parent.type === \"ConditionalExpression\" && node.parent.test === node){ return true; }\n  return isInsideConditionalExpressionTest(node.parent);\n}\n\nexport function isInsideDisallowedBinaryExpression(node: WithParent<ESNode>): boolean {\n  if(!hasESNodeParentExtension(node)){ return false; }\n  if(\n    node.parent.type === \"BinaryExpression\" &&\n    node.parent.operator !== \"+\" // allow string concatenation\n  ){ return true; }\n  return isInsideDisallowedBinaryExpression(node.parent);\n}\n\nexport function isInsideLogicalExpressionLeft(node: WithParent<ESNode>): boolean {\n  if(!hasESNodeParentExtension(node)){ return false; }\n  if(node.parent.type === \"LogicalExpression\" && node.parent.left === node){ return true; }\n  return isInsideLogicalExpressionLeft(node.parent);\n}\n\nexport function isInsideMemberExpression(node: WithParent<ESNode>): boolean {\n  // aka indexed access: https://github.com/estree/estree/blob/master/es5.md#memberexpression\n  if(!hasESNodeParentExtension(node)){ return false; }\n  if(node.parent.type === \"MemberExpression\"){ return true; }\n  return isInsideMemberExpression(node.parent);\n}\n\nexport function isIndexedAccessLiteral(node: WithParent<ESNode>): boolean {\n  if(!hasESNodeParentExtension(node)){ return false; }\n  if(node.parent.type !== \"MemberExpression\"){ return false; }\n  return node.parent.property === node && isESSimpleStringLiteral(node);\n}\n"
  },
  {
    "path": "src/utils/quotes.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { escapeNestedQuotes } from \"better-tailwindcss:utils/quotes.js\";\n\n\ndescribe(\"escapeNestedQuotes\", () => {\n  it(\"should escape all nested quotes\", () => {\n    expect(escapeNestedQuotes('content-[\"\"]', \"\\\"\")).toBe('content-[\\\\\"\\\\\"]');\n    expect(escapeNestedQuotes(\"content-['']\", \"'\")).toBe(\"content-[\\\\'\\\\']\");\n  });\n\n  it(\"should not escape quotes that are not nested\", () => {\n    expect(escapeNestedQuotes('content-[\"\"]', \"'\")).toBe('content-[\"\"]');\n    expect(escapeNestedQuotes(\"content-['']\", \"\\\"\")).toBe(\"content-['']\");\n  });\n});\n"
  },
  {
    "path": "src/utils/quotes.ts",
    "content": "import type { LiteralValueQuotes } from \"better-tailwindcss:types/ast.js\";\n\n\nexport function escapeNestedQuotes(content: string, surroundingQuotes: LiteralValueQuotes): string {\n  const regex = surroundingQuotes === \"'\"\n    ? /(?<!\\\\)'/g\n    : surroundingQuotes === \"\\\"\"\n      ? /(?<!\\\\)\"/g\n      : /(?<!\\\\)`/g;\n\n  return content.replace(regex, `\\\\${surroundingQuotes}`);\n}\n"
  },
  {
    "path": "src/utils/rule.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\n\nimport { toJsonSchema } from \"@valibot/to-json-schema\";\nimport { getDefaults, strictObject } from \"valibot\";\n\nimport { COMMON_OPTIONS } from \"better-tailwindcss:options/descriptions.js\";\nimport { migrateLegacySelectorsToFlatSelectors } from \"better-tailwindcss:options/migrate.js\";\nimport { getAttributesByAngularElement, getLiteralsByAngularAttribute } from \"better-tailwindcss:parsers/angular.js\";\nimport { getLiteralsByCSSAtRule } from \"better-tailwindcss:parsers/css.js\";\nimport {\n  getLiteralsByESBareTemplateLiteral,\n  getLiteralsByESCallExpression,\n  getLiteralsByESExportDefaultDeclaration,\n  getLiteralsByESVariableDeclarator,\n  getLiteralsByTaggedTemplateExpression\n} from \"better-tailwindcss:parsers/es.js\";\nimport { getAttributesByHTMLTag, getLiteralsByHTMLAttribute } from \"better-tailwindcss:parsers/html.js\";\nimport { getAttributesByJSXElement, getLiteralsByJSXAttribute } from \"better-tailwindcss:parsers/jsx.js\";\nimport {\n  getAttributesBySvelteTag,\n  getDirectivesBySvelteTag,\n  getLiteralsBySvelteAttribute,\n  getLiteralsBySvelteDirective\n} from \"better-tailwindcss:parsers/svelte.js\";\nimport { getAttributesByVueStartTag, getLiteralsByVueAttribute } from \"better-tailwindcss:parsers/vue.js\";\nimport { SelectorKind } from \"better-tailwindcss:types/rule.js\";\nimport { getLocByRange } from \"better-tailwindcss:utils/ast.js\";\nimport { resolveJson } from \"better-tailwindcss:utils/resolvers.js\";\nimport { augmentMessageWithWarnings, escapeMessage } from \"better-tailwindcss:utils/utils.js\";\nimport { removeDefaults } from \"better-tailwindcss:utils/valibot.js\";\nimport { parseSemanticVersion } from \"better-tailwindcss:utils/version.js\";\nimport { warnOnce } from \"better-tailwindcss:utils/warn.js\";\n\nimport type { TmplAstElement } from \"@angular-eslint/bundled-angular-compiler\";\nimport type { Atrule } from \"@eslint/css-tree\";\nimport type { TagNode } from \"es-html-parser\";\nimport type { Rule } from \"eslint\";\nimport type {\n  CallExpression,\n  ExportDefaultDeclaration,\n  Node,\n  TaggedTemplateExpression,\n  TemplateLiteral,\n  VariableDeclarator\n} from \"estree\";\nimport type { JSXOpeningElement } from \"estree-jsx\";\nimport type { SvelteStartTag } from \"svelte-eslint-parser/lib/ast/index.js\";\nimport type { AST } from \"vue-eslint-parser\";\n\nimport type { CommonOptions } from \"better-tailwindcss:options/descriptions.js\";\nimport type { Literal } from \"better-tailwindcss:types/ast.js\";\nimport type {\n  AttributeSelector,\n  CalleeSelector,\n  Context,\n  CreateRuleOptions,\n  ESLintRule,\n  JsonSchema,\n  RuleCategory,\n  RuleContext,\n  Schema,\n  Selectors,\n  TagSelector,\n  VariableSelector\n} from \"better-tailwindcss:types/rule.js\";\n\n\nexport function createRule<\n  const Name extends string,\n  const Messages extends Record<string, string>,\n  const OptionsSchema extends Schema = Schema,\n  const Options extends CommonOptions & JsonSchema<OptionsSchema> = CommonOptions & JsonSchema<OptionsSchema>,\n  const Category extends RuleCategory = RuleCategory,\n  const Recommended extends boolean = boolean\n>(options: CreateRuleOptions<Name, Messages, OptionsSchema, Options, Category, Recommended>) {\n\n  const { autofix, category, description, docs, initialize, lintLiterals, messages, name, recommended, schema } = options;\n\n  let eslintContext: Rule.RuleContext | undefined;\n\n  const propertiesSchema = strictObject({\n    // eslint injects the defaults from the settings to options, if not specified in the options\n    // because we want to have a specific order of precedence, we need to remove the defaults here and merge them\n    // manually in getOptions. The order of precedence is:\n    // 1. defaults from settings\n    // 2. defaults from option\n    // 3. configs from settings\n    // 4. configs from option\n    ...removeDefaults(COMMON_OPTIONS.entries),\n    ...schema?.entries\n  });\n\n  const jsonSchema = toJsonSchema(propertiesSchema).properties;\n\n  const getOptions = (): Options => {\n    const defaultSettings = getDefaults(COMMON_OPTIONS);\n    const defaultOptions = schema ? getDefaults(schema) : {};\n    const settings = eslintContext?.settings?.[\"eslint-plugin-better-tailwindcss\"] ?? eslintContext?.settings?.[\"better-tailwindcss\"] ?? {};\n    const options = eslintContext?.options[0] ?? {};\n\n    const mergedOptions = {\n      ...defaultSettings,\n      ...defaultOptions,\n      ...settings,\n      ...options\n    };\n\n    const migratedSelectors = migrateLegacySelectorsToFlatSelectors({\n      attributes: mergedOptions.attributes,\n      callees: mergedOptions.callees,\n      tags: mergedOptions.tags,\n      variables: mergedOptions.variables\n    });\n\n    const hasAttributeOverride = mergedOptions.attributes !== undefined;\n    const hasCalleeOverride = mergedOptions.callees !== undefined;\n    const hasTagOverride = mergedOptions.tags !== undefined;\n    const hasVariableOverride = mergedOptions.variables !== undefined;\n\n    const preservedSelectors = (mergedOptions.selectors ?? []).filter(selector => {\n      if(hasAttributeOverride && selector.kind === SelectorKind.Attribute){\n        return false;\n      }\n\n      if(hasCalleeOverride && selector.kind === SelectorKind.Callee){\n        return false;\n      }\n\n      if(hasTagOverride && selector.kind === SelectorKind.Tag){\n        return false;\n      }\n\n      if(hasVariableOverride && selector.kind === SelectorKind.Variable){\n        return false;\n      }\n\n      return true;\n    });\n\n    const selectors = [\n      ...migratedSelectors,\n      ...preservedSelectors\n    ];\n\n    return {\n      ...mergedOptions,\n      selectors\n    };\n  };\n\n  return {\n    category,\n    messages,\n    name,\n    get options() { return getOptions(); },\n    recommended,\n    rule: {\n      create: ctx => {\n\n        eslintContext = ctx;\n\n        const options = getOptions();\n\n        const { messageStyle } = options;\n\n        // #361#issuecomment-4227041592\n        const cwd = options.cwd\n          ? resolve(ctx.cwd, options.cwd)\n          : ctx.cwd;\n\n        const packageJsonPath = resolveJson(\"tailwindcss/package.json\", cwd);\n\n        if(!packageJsonPath){\n          warnOnce(`Tailwind CSS is not installed. Disabling rule ${ctx.id}.`);\n          return {};\n        }\n\n        const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n        const version = parseSemanticVersion(packageJson.version);\n        const installation = dirname(packageJsonPath);\n\n        const context = {\n          cwd,\n          docs,\n          installation,\n          options,\n          report: ({ fix, range, warnings, ...rest }) => {\n            const loc = getLocByRange(ctx, range);\n\n            if(\"id\" in rest && rest.id && messages && rest.id in messages){\n              return void ctx.report({\n                data: rest.data,\n                loc,\n                ...fix !== undefined && {\n                  fix: fixer => fixer.replaceTextRange(range, fix)\n                },\n                message: escapeMessage(messageStyle, augmentMessageWithWarnings(messages[rest.id], docs, warnings))\n              });\n            }\n\n            if(\"message\" in rest && rest.message){\n              return void ctx.report({\n                loc,\n                ...fix !== undefined && {\n                  fix: fixer => fixer.replaceTextRange(range, fix)\n                },\n                message: escapeMessage(messageStyle, augmentMessageWithWarnings(rest.message, docs, warnings))\n              });\n            }\n          },\n          version\n        } satisfies RuleContext<Messages, Options>;\n\n        initialize?.(context);\n\n        return createRuleListener(eslintContext, context, lintLiterals);\n      },\n      meta: {\n        docs: {\n          description,\n          recommended,\n          url: docs\n        },\n        fixable: autofix ? \"code\" : undefined,\n        schema: [\n          {\n            additionalProperties: false,\n            properties: jsonSchema,\n            type: \"object\"\n          }\n        ],\n        type: category === \"correctness\" ? \"problem\" : \"layout\",\n        ...messages && { messages }\n      }\n    }\n  } satisfies ESLintRule<Name, Messages, Options, Category, Recommended>;\n}\n\nexport function createRuleListener<Ctx extends Context>(ctx: Rule.RuleContext, context: Ctx, lintLiterals: (ctx: Ctx, literals: Literal[]) => void): Rule.RuleListener {\n\n  const selectors = context.options.selectors as Selectors;\n\n  const attributes: AttributeSelector[] = [];\n  const callees: CalleeSelector[] = [];\n  const tags: TagSelector[] = [];\n  const variables: VariableSelector[] = [];\n\n  for(const selector of selectors){\n    switch (selector.kind){\n      case SelectorKind.Attribute:\n        attributes.push(selector);\n        break;\n      case SelectorKind.Callee:\n        callees.push(selector);\n        break;\n      case SelectorKind.Tag:\n        tags.push(selector);\n        break;\n      case SelectorKind.Variable:\n        variables.push(selector);\n        break;\n    }\n  }\n\n  const callExpression = {\n    CallExpression(node: Node) {\n      const callExpressionNode = node as CallExpression;\n\n      const literals = getLiteralsByESCallExpression(ctx, callExpressionNode, callees);\n\n      if(literals.length > 0){\n        lintLiterals(context, literals);\n      }\n    }\n  };\n\n  const variableDeclarators = {\n    VariableDeclarator(node: Node) {\n      const variableDeclaratorNode = node as VariableDeclarator;\n\n      const literals = getLiteralsByESVariableDeclarator(ctx, variableDeclaratorNode, variables);\n\n      if(literals.length > 0){\n        lintLiterals(context, literals);\n      }\n    }\n  };\n\n  const exportDefaultDeclarations = {\n    ExportDefaultDeclaration(node: Node) {\n      const exportDefaultDeclarationNode = node as ExportDefaultDeclaration;\n\n      const literals = getLiteralsByESExportDefaultDeclaration(ctx, exportDefaultDeclarationNode, variables);\n\n      if(literals.length > 0){\n        lintLiterals(context, literals);\n      }\n    }\n  };\n\n  const taggedTemplateExpression = {\n    TaggedTemplateExpression(node: Node) {\n      const taggedTemplateExpressionNode = node as TaggedTemplateExpression;\n\n      const literals = getLiteralsByTaggedTemplateExpression(ctx, taggedTemplateExpressionNode, tags);\n\n      if(literals.length > 0){\n        lintLiterals(context, literals);\n      }\n    }\n  };\n\n  const bareTemplateLiteral = {\n    TemplateLiteral(node: Node) {\n      const templateLiteralNode = node as TemplateLiteral;\n\n      const literals = getLiteralsByESBareTemplateLiteral(ctx, templateLiteralNode, tags);\n\n      if(literals.length > 0){\n        lintLiterals(context, literals);\n      }\n    }\n  };\n\n  const jsx = {\n    JSXOpeningElement(node: Node) {\n      const jsxNode = node as JSXOpeningElement;\n      const jsxAttributes = getAttributesByJSXElement(ctx, jsxNode);\n\n      for(const jsxAttribute of jsxAttributes){\n\n        const attributeValue = jsxAttribute.value;\n\n        if(!attributeValue){ continue; }\n\n        const literals = getLiteralsByJSXAttribute(ctx, jsxAttribute, attributes);\n\n        if(literals.length > 0){\n          lintLiterals(context, literals);\n        }\n      }\n    }\n  };\n\n  const svelte = {\n    SvelteStartTag(node: Node) {\n      const svelteNode = node as unknown as SvelteStartTag;\n      const svelteAttributes = getAttributesBySvelteTag(ctx, svelteNode);\n      const svelteDirectives = getDirectivesBySvelteTag(ctx, svelteNode);\n\n      for(const svelteAttribute of svelteAttributes){\n        const attributeName = svelteAttribute.key.name;\n\n        if(typeof attributeName !== \"string\"){ continue; }\n\n        const literals = getLiteralsBySvelteAttribute(ctx, svelteAttribute, attributes);\n\n        if(literals.length > 0){\n          lintLiterals(context, literals);\n        }\n      }\n\n      for(const svelteDirective of svelteDirectives){\n        const literals = getLiteralsBySvelteDirective(ctx, svelteDirective, attributes);\n\n        if(literals.length > 0){\n          lintLiterals(context, literals);\n        }\n      }\n    }\n  };\n\n  const vue = {\n    VStartTag(node: Node) {\n      const vueNode = node as unknown as AST.VStartTag;\n      const vueAttributes = getAttributesByVueStartTag(ctx, vueNode);\n\n      for(const attribute of vueAttributes){\n        const literals = getLiteralsByVueAttribute(ctx, attribute, attributes);\n\n        if(literals.length > 0){\n          lintLiterals(context, literals);\n        }\n      }\n    }\n  };\n\n  const html = {\n    Tag(node: Node) {\n      const htmlTagNode = node as unknown as TagNode;\n      const htmlAttributes = getAttributesByHTMLTag(ctx, htmlTagNode);\n\n      for(const htmlAttribute of htmlAttributes){\n        const literals = getLiteralsByHTMLAttribute(ctx, htmlAttribute, attributes);\n\n        if(literals.length > 0){\n          lintLiterals(context, literals);\n        }\n      }\n    }\n  };\n\n  const angular = {\n    Element(node: Node) {\n      const angularElementNode = node as unknown as TmplAstElement;\n      const angularAttributes = getAttributesByAngularElement(ctx, angularElementNode);\n\n      for(const angularAttribute of angularAttributes){\n        const literals = getLiteralsByAngularAttribute(ctx, angularAttribute, attributes);\n\n        if(literals.length > 0){\n          lintLiterals(context, literals);\n        }\n      }\n    }\n  };\n\n  const css = {\n    Atrule(node: Node) {\n      const atRuleNode = node as unknown as Atrule;\n\n      const literals = getLiteralsByCSSAtRule(ctx, atRuleNode);\n\n      if(literals.length > 0){\n        lintLiterals(context, literals);\n      }\n    }\n  };\n\n  // Vue\n  if(typeof ctx.sourceCode.parserServices?.defineTemplateBodyVisitor === \"function\"){\n    return {\n      // script tag\n      ...callExpression,\n      ...variableDeclarators,\n      ...bareTemplateLiteral,\n      ...exportDefaultDeclarations,\n      ...taggedTemplateExpression,\n\n      // bound classes\n      ...ctx.sourceCode.parserServices.defineTemplateBodyVisitor({\n        ...callExpression,\n        ...vue\n      })\n    };\n  }\n\n  return {\n    ...callExpression,\n    ...variableDeclarators,\n    ...bareTemplateLiteral,\n    ...exportDefaultDeclarations,\n    ...taggedTemplateExpression,\n    ...jsx,\n    ...svelte,\n    ...vue,\n    ...html,\n    ...angular,\n    ...css\n  };\n}\n"
  },
  {
    "path": "src/utils/selectors.ts",
    "content": "import type { Selector, SelectorByKind, SelectorKind } from \"better-tailwindcss:types/rule.js\";\n\n\nexport function isSelectorKind<Kind extends SelectorKind>(kind: Kind) {\n  return (selector: Selector): selector is SelectorByKind<Kind> => selector.kind === kind;\n}\n"
  },
  {
    "path": "src/utils/utils.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { matchesName } from \"better-tailwindcss:utils/utils.js\";\n\nimport { escapeForRegex } from \"../async-utils/escape\";\n\n\ndescribe(\"matchesName\", () => {\n  it(\"should match name\", () => {\n    expect(matchesName(\"class\", \"class\")).toBe(true);\n    expect(matchesName(\"data-attribute\", \"data-attribute\")).toBe(true);\n    expect(matchesName(\"custom_variable\", \"custom_variable\")).toBe(true);\n  });\n\n  it(\"should not match partial matches\", () => {\n    expect(matchesName(\"class\", \"className\")).toBe(false);\n  });\n\n  it(\"should match by regex\", () => {\n    expect(matchesName(\"class.*\", \"className\")).toBe(true);\n    expect(matchesName(\"class$\", \"class$\")).toBe(false);\n    expect(matchesName(\"class\\\\$\", \"class$\")).toBe(true);\n  });\n});\n\ndescribe(\"escapeForRegex\", () => {\n  it(\"should escape an user provided string to be used in a regular expression\", () => {\n    expect(escapeForRegex(\".*\")).toBe(\"\\\\.\\\\*\");\n    expect(escapeForRegex(\"hello?\")).toBe(\"hello\\\\?\");\n    expect(escapeForRegex(\"[abc]\")).toBe(\"\\\\[abc\\\\]\");\n    expect(escapeForRegex(\"a+b*c\")).toBe(\"a\\\\+b\\\\*c\");\n    expect(escapeForRegex(\"class$\")).toBe(\"class\\\\$\");\n  });\n});\n"
  },
  {
    "path": "src/utils/utils.ts",
    "content": "import { getCachedRegex } from \"better-tailwindcss:utils/regex.js\";\n\nimport type { MessageStyleOption } from \"better-tailwindcss:options/schemas/common.js\";\nimport type { BracesMeta, Literal, QuoteMeta } from \"better-tailwindcss:types/ast.js\";\nimport type { Warning } from \"better-tailwindcss:types/async.js\";\n\n\nexport function getWhitespace(classes: string) {\n  const leadingWhitespace = classes.match(/^\\s*/)?.[0];\n  const trailingWhitespace = classes.match(/\\s*$/)?.[0];\n\n  return { leadingWhitespace, trailingWhitespace };\n}\n\nexport function getQuotes(raw: string): QuoteMeta {\n  const openingQuote = raw.at(0);\n  const closingQuote = raw.at(-1);\n\n  return {\n    closingQuote: closingQuote === \"'\" || closingQuote === '\"' || closingQuote === \"`\" ? closingQuote : undefined,\n    openingQuote: openingQuote === \"'\" || openingQuote === '\"' || openingQuote === \"`\" ? openingQuote : undefined\n  };\n}\n\nexport function getContent(raw: string, quotes?: QuoteMeta, braces?: BracesMeta) {\n  return raw.substring(\n    (quotes?.openingQuote?.length ?? 0) + (braces?.closingBraces?.length ?? 0),\n    raw.length - (quotes?.closingQuote?.length ?? 0) - (braces?.openingBraces?.length ?? 0)\n  );\n}\n\nexport function splitClasses(classes: string): string[] {\n  if(classes.trim() === \"\"){\n    return [];\n  }\n\n  return classes\n    .trim()\n    .split(/\\s+/);\n}\n\nexport function deduplicateClasses(classes: string[]): string[] {\n  return classes.filter((className, index) => {\n    return classes.indexOf(className) === index;\n  });\n}\n\nexport function display(messageStyle: MessageStyleOption[\"messageStyle\"], classes: string): string {\n  if(messageStyle === \"raw\"){\n    return escapeMessage(messageStyle, classes);\n  }\n\n  return escapeMessage(\n    messageStyle,\n    classes\n      .replaceAll(\" \", \"·\")\n      .replaceAll(\"\\n\", \"↵\\n\")\n      .replaceAll(\"\\r\", \"↩\\r\")\n      .replaceAll(\"\\t\", \"→\")\n  );\n}\n\n/**\n * Augments a message with additional warnings and documentation links.\n *\n * @template Options\n * @param message The original message to augment.\n * @param docs The documentation URL to include.\n * @param warnings Any warnings to include in the message.\n * @returns The augmented message.\n */\nexport function augmentMessageWithWarnings<Options extends Record<string, any>>(message: string, docs: string, warnings?: (Warning<Options> | undefined)[]) {\n  const ruleWarnings = warnings\n    ?.filter(warning => !!warning)\n    .map(warning => ({ ...warning, url: warning.url ?? docs }));\n\n  if(!ruleWarnings || ruleWarnings.length === 0){\n    return message;\n  }\n\n  return [\n    ruleWarnings.flatMap(({ option, title, url }) => [\n      `⚠️ Warning: ${title}. Option \\`${option}\\` may be misconfigured.`,\n      `Check documentation at ${url}`\n    ]).join(\"\\n\"),\n    message\n  ].join(\"\\n\\n\");\n}\n\nexport function escapeMessage(messageStyle: MessageStyleOption[\"messageStyle\"], message: string): string {\n  if(messageStyle === \"compact\"){\n    return message\n      .replaceAll(\"\\r\", \"\")\n      .replaceAll(\"\\n\", \"\");\n  }\n\n  return message;\n}\n\nexport function splitWhitespaces(classes: string): string[] {\n  return classes.split(/\\S+/);\n}\n\nexport function getIndentation(line: string): number {\n  return line.match(/^[\\t ]*/)?.[0].length ?? 0;\n}\n\nexport function isClassSticky(literal: Literal, classIndex: number): boolean {\n  const classes = literal.content;\n\n  const classChunks = splitClasses(classes);\n  const whitespaceChunks = splitWhitespaces(classes);\n\n  const startsWithWhitespace = whitespaceChunks.length > 0 && whitespaceChunks[0] !== \"\";\n  const endsWithWhitespace = whitespaceChunks.length > 0 && whitespaceChunks[whitespaceChunks.length - 1] !== \"\";\n\n  return (\n    !startsWithWhitespace && classIndex === 0 && !!literal.closingBraces ||\n    !endsWithWhitespace && classIndex === classChunks.length - 1 && !!literal.openingBraces\n  );\n}\n\nexport function getExactClassLocation(literal: Literal, startIndex: number, endIndex: number) {\n  const linesUpToStartIndex = literal.content.slice(0, startIndex).split(/\\r?\\n/);\n  const isOnFirstLine = linesUpToStartIndex.length === 1;\n  const containingLine = linesUpToStartIndex.at(-1);\n\n  const line = literal.loc.start.line + linesUpToStartIndex.length - 1;\n  const column = (\n    isOnFirstLine\n      ? literal.loc.start.column + (literal.openingQuote?.length ?? 0) + (literal.closingBraces?.length ?? 0)\n      : 0\n  ) + (containingLine?.length ?? 0);\n\n  return {\n    end: {\n      column: column + (endIndex - startIndex),\n      line\n    },\n    start: {\n      column,\n      line\n    }\n  };\n}\n\nexport function matchesName(pattern: string, name: string | undefined): boolean {\n  if(!name){ return false; }\n\n  const match = getCachedRegex(pattern).exec(name);\n  return !!match && match[0] === name;\n}\n\nexport function replacePlaceholders(template: string, match: RegExpMatchArray | string[]): string {\n  return template.replace(getCachedRegex(/\\$(\\d+)/g), (_, groupIndex) => {\n    const index = Number(groupIndex);\n    return match[index] ?? \"\";\n  });\n}\n\nexport function addAttribute(name: string | undefined): (literal: Literal, index: number, literals: Literal[]) => Literal {\n  return (literal: Literal) => {\n    if(!name){\n      return literal;\n    }\n\n    literal.attribute = name;\n    return literal;\n  };\n}\n\nexport function deduplicateLiterals(literal: Literal, index: number, literals: Literal[]): boolean {\n  return literals.findIndex(l2 => {\n    return literal.content === l2.content &&\n      literal.range[0] === l2.range[0] &&\n      literal.range[1] === l2.range[1];\n  }) === index;\n}\n\nexport function createObjectPathElement(path?: string): string {\n  if(!path){ return \"\"; }\n\n  return getCachedRegex(/^[A-Z_a-z]\\w*$/).test(path)\n    ? path\n    : `[\"${path}\"]`;\n}\n\nexport interface GenericNodeWithParent {\n  parent: Partial<GenericNodeWithParent>;\n}\n\nexport function isGenericNodeWithParent(node: unknown): node is GenericNodeWithParent {\n  return (\n    typeof node === \"object\" &&\n    node !== null &&\n    \"parent\" in node &&\n    node.parent !== null &&\n    typeof node.parent === \"object\"\n  );\n}\n"
  },
  {
    "path": "src/utils/valibot.ts",
    "content": "export function removeDefaults(entries: Record<string, any>) {\n  const newEntries = { ...entries };\n  for(const key in newEntries){\n    const schema = { ...newEntries[key] };\n    if(\"default\" in schema){\n      delete schema.default;\n    }\n    newEntries[key] = schema;\n  }\n  return newEntries;\n}\n"
  },
  {
    "path": "src/utils/version.ts",
    "content": "export function parseSemanticVersion(version: string): { major: number; minor: number; patch: number; identifier?: string; } {\n  const [major, minor, patchString] = version.split(\".\");\n  const [patch, identifier] = patchString.split(\"-\");\n\n  return { identifier, major: +major, minor: +minor, patch: +patch };\n}\n"
  },
  {
    "path": "src/utils/warn.ts",
    "content": "const warnedMessages = new Set<string>();\n\nexport function warnOnce(message: string) {\n  if(!warnedMessages.has(message)){\n    console.warn(\"⚠️ Warning:\", message);\n    warnedMessages.add(message);\n  }\n}\n"
  },
  {
    "path": "tests/e2e/commonjs/eslint.config.js",
    "content": "const eslintParserHTML = require(\"@html-eslint/parser\");\nconst eslintPluginBetterTailwindcss = require(\"eslint-plugin-better-tailwindcss\");\n\n\nmodule.exports = {\n  ...eslintPluginBetterTailwindcss.configs[\"stylistic-warn\"],\n\n  files: [\"**/*.html\"],\n  languageOptions: {\n    parser: eslintParserHTML\n  }\n};\n"
  },
  {
    "path": "tests/e2e/commonjs/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "tests/e2e/commonjs/test.html",
    "content": "<!DOCTYPE html>\n  <head></head>\n  <body>\n    <div class=\" hover:font-bold font-bold font-bold\">\n  </body>\n</html>"
  },
  {
    "path": "tests/e2e/commonjs/test.test.ts",
    "content": "import { loadESLint } from \"eslint\";\nimport { describe, expect, it } from \"vitest\";\n\n\ndescribe(\"e2e/commonjs\", async () => {\n  it(\"should report all errors\", async () => {\n    const ESLint = await loadESLint({ useFlatConfig: true });\n\n    const eslint = new ESLint({\n      cwd: import.meta.dirname,\n      overrideConfigFile: \"./eslint.config.js\"\n    });\n\n    const [json] = await eslint.lintFiles(\"./test.html\");\n\n    expect(json.errorCount).toBe(0);\n    expect(json.fatalErrorCount).toBe(0);\n    expect(json.fixableErrorCount).toBe(0);\n    expect(json.fixableWarningCount).toBe(4);\n    expect(json.warningCount).toBe(4);\n\n    expect(json.messages.map(({ ruleId }) => ruleId)).toEqual([\n      \"better-tailwindcss/enforce-consistent-class-order\",\n      \"better-tailwindcss/enforce-consistent-line-wrapping\",\n      \"better-tailwindcss/no-unnecessary-whitespace\",\n      \"better-tailwindcss/no-duplicate-classes\"\n    ]);\n\n  });\n});\n"
  },
  {
    "path": "tests/e2e/eslintrc/.eslintrc.json",
    "content": "{\n  \"extends\": [\n    \"plugin:better-tailwindcss/legacy-stylistic-warn\"\n  ],\n\n  \"parser\": \"@html-eslint/parser\"\n}\n"
  },
  {
    "path": "tests/e2e/eslintrc/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "tests/e2e/eslintrc/test.html",
    "content": "<!DOCTYPE html>\n  <head></head>\n  <body>\n    <div class=\" hover:font-bold font-bold font-bold\">\n  </body>\n</html>"
  },
  {
    "path": "tests/e2e/eslintrc/test.test.ts",
    "content": "import { loadESLint } from \"eslint\";\nimport { describe, expect, it } from \"vitest\";\n\n\ndescribe(\"e2e/eslintrc\", async () => {\n  it(\"should report all errors\", async () => {\n    const ESLint = await loadESLint({ useFlatConfig: false });\n\n    const eslint = new ESLint({\n      cwd: import.meta.dirname,\n      overrideConfigFile: \"./.eslintrc.json\"\n    });\n\n    const [json] = await eslint.lintFiles(\"./test.html\");\n\n    expect(json.errorCount).toBe(0);\n    expect(json.fatalErrorCount).toBe(0);\n    expect(json.fixableErrorCount).toBe(0);\n    expect(json.fixableWarningCount).toBe(4);\n    expect(json.warningCount).toBe(4);\n\n    expect(json.messages.map(({ ruleId }) => ruleId)).toEqual([\n      \"better-tailwindcss/enforce-consistent-class-order\",\n      \"better-tailwindcss/enforce-consistent-line-wrapping\",\n      \"better-tailwindcss/no-unnecessary-whitespace\",\n      \"better-tailwindcss/no-duplicate-classes\"\n    ]);\n\n  });\n});\n"
  },
  {
    "path": "tests/e2e/esm/eslint.config.js",
    "content": "import eslintParserHTML from \"@html-eslint/parser\";\nimport eslintPluginBetterTailwindcss from \"eslint-plugin-better-tailwindcss\";\n\n\nexport default {\n  ...eslintPluginBetterTailwindcss.configs[\"stylistic-warn\"],\n\n  files: [\"**/*.html\"],\n  languageOptions: {\n    parser: eslintParserHTML\n  }\n};\n"
  },
  {
    "path": "tests/e2e/esm/package.json",
    "content": "{\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "tests/e2e/esm/test.html",
    "content": "<!DOCTYPE html>\n  <head></head>\n  <body>\n    <div class=\" hover:font-bold font-bold font-bold\">\n  </body>\n</html>"
  },
  {
    "path": "tests/e2e/esm/test.test.ts",
    "content": "import { loadESLint } from \"eslint\";\nimport { describe, expect, it } from \"vitest\";\n\n\ndescribe(\"e2e/esm\", async () => {\n  it(\"should report all errors\", async () => {\n    const ESLint = await loadESLint({ useFlatConfig: true });\n\n    const eslint = new ESLint({\n      cwd: import.meta.dirname,\n      overrideConfigFile: \"./eslint.config.js\"\n    });\n\n    const [json] = await eslint.lintFiles(\"./test.html\");\n\n    expect(json.errorCount).toBe(0);\n    expect(json.fatalErrorCount).toBe(0);\n    expect(json.fixableErrorCount).toBe(0);\n    expect(json.fixableWarningCount).toBe(4);\n    expect(json.warningCount).toBe(4);\n\n    expect(json.messages.map(({ ruleId }) => ruleId)).toEqual([\n      \"better-tailwindcss/enforce-consistent-class-order\",\n      \"better-tailwindcss/enforce-consistent-line-wrapping\",\n      \"better-tailwindcss/no-unnecessary-whitespace\",\n      \"better-tailwindcss/no-duplicate-classes\"\n    ]);\n\n  });\n});\n"
  },
  {
    "path": "tests/unit/monorepo-cwd-resolution.test.ts",
    "content": "import { loadESLint } from \"eslint\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n\nimport eslintPluginBetterTailwindCSS from \"better-tailwindcss:configs/config.js\";\nimport { TEST_SYNTAXES } from \"better-tailwindcss:tests/utils/lint.js\";\nimport { TestDirectory } from \"better-tailwindcss:tests/utils/tmp.js\";\n\n\n// Simulates a monorepo where:\n// ./           <-- linter cwd (no tailwindcss here)\n// ./packages/website/node_modules/tailwindcss/  <-- installed here\n// ./packages/website/src/index.tsx  <-- file being linted\n\ndescribe(\"monorepo cwd resolution\", async () => {\n  let fs: TestDirectory;\n\n  beforeEach(() => {\n    fs = new TestDirectory({\n      \"packages/website/node_modules/tailwindcss/index.css\": \"\",\n      \"packages/website/node_modules/tailwindcss/package.json\": JSON.stringify({\n        name: \"tailwindcss\",\n        style: \"index.css\",\n        version: \"4.0.0\"\n      }),\n      \"packages/website/node_modules/tailwindcss/theme.css\": \"\",\n      \"packages/website/src/global.css\": \"\",\n      \"packages/website/src/index.jsx\": /* tsx */`\n        export default () => <div class=\" flex \" />;\n      `\n    }, true);\n  });\n\n  afterEach(() => {\n    fs.cleanUp();\n  });\n\n  const ESLint = await loadESLint({ useFlatConfig: true });\n\n  it(\"should resolve tailwindcss from the explicitly configured cwd\", async () => {\n    const linter = new ESLint({\n      cwd: fs.directory,\n      overrideConfig: {\n        ...TEST_SYNTAXES.jsx,\n        files: [\"**/*.jsx\"],\n        plugins: {\n          \"better-tailwindcss\": eslintPluginBetterTailwindCSS\n        },\n        rules: {\n          \"better-tailwindcss/no-unnecessary-whitespace\": \"warn\"\n        },\n        settings: {\n          \"better-tailwindcss\": {\n            cwd: \"packages/website/\"\n          }\n        }\n      },\n      overrideConfigFile: true\n    });\n\n    const results = await linter.lintFiles(fs.files[\"packages/website/src/index.jsx\"].path);\n\n    expect(results).toHaveLength(1);\n    expect(results[0].messages).toHaveLength(2);\n    expect(results[0].messages[0].ruleId).toBe(\"better-tailwindcss/no-unnecessary-whitespace\");\n    expect(results[0].messages[1].ruleId).toBe(\"better-tailwindcss/no-unnecessary-whitespace\");\n  });\n\n});\n"
  },
  {
    "path": "tests/unit/options.test.ts",
    "content": "import { describe, it } from \"vitest\";\n\nimport { noDuplicateClasses } from \"better-tailwindcss:rules/no-duplicate-classes.js\";\nimport { lint } from \"better-tailwindcss:tests/utils/lint.js\";\n\n\ndescribe(\"settings\", () => {\n\n  it(\"should use the global settings if provided\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img settings=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img settings=\"  b  a  c    \" />`,\n          jsx: `() => <img settings=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img settings=\"  b  a  c    \" />`,\n          svelte: `<img settings=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img settings=\"  b  a  c    \" />`,\n          vue: `<template><img settings=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img settings=\"  b  a  c    \" /></template>`,\n\n          errors: 1,\n          settings: { \"better-tailwindcss\": { attributes: [\"settings\"] } }\n        }\n      ]\n    });\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img settings=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img settings=\"  b  a  c    \" />`,\n          jsx: `() => <img settings=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img settings=\"  b  a  c    \" />`,\n          svelte: `<img settings=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img settings=\"  b  a  c    \" />`,\n          vue: `<template><img settings=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img settings=\"  b  a  c    \" /></template>`,\n\n          errors: 1,\n          settings: { \"eslint-plugin-better-tailwindcss\": { attributes: [\"settings\"] } }\n        }\n      ]\n    });\n  });\n\n  it(\"should always use rule options to override settings if provided\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img options=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img options=\"  b  a  c    \" />`,\n          jsx: `() => <img options=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img options=\"  b  a  c    \" />`,\n          svelte: `<img options=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img options=\"  b  a  c    \" />`,\n          vue: `<template><img options=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img options=\"  b  a  c    \" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [\"options\"] }],\n          settings: { \"better-tailwindcss\": { attributes: [\"settings\"] } }\n        }\n      ]\n    });\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img options=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img options=\"  b  a  c    \" />`,\n          jsx: `() => <img options=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img options=\"  b  a  c    \" />`,\n          svelte: `<img options=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img options=\"  b  a  c    \" />`,\n          vue: `<template><img options=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img options=\"  b  a  c    \" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [\"options\"] }],\n          settings: { \"eslint-plugin-better-tailwindcss\": { attributes: [\"settings\"] } }\n        }\n      ]\n    });\n  });\n\n  it(\"should only override provided settings on defaults\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img settings=\"  b  a  c    \" class=\"  b  a  c  a  \" />`,\n          jsx: `() => <img settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img settings=\"  b  a  c    \" class=\"  b  a  c  a  \" />`,\n          svelte: `<img settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img settings=\"  b  a  c    \" class=\"  b  a  c  a  \" />`,\n          vue: `<template><img settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img settings=\"  b  a  c    \" class=\"  b  a  c  a  \" /></template>`,\n\n          errors: 1,\n          settings: { \"better-tailwindcss\": { attributes: [\"settings\"] } }\n        }\n      ]\n    });\n  });\n\n  it(\"should only override provided options on settings\", () => {\n    lint(noDuplicateClasses, {\n      invalid: [\n        {\n          html: `<img options=\"  b  a  c  a  \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          htmlOutput: `<img options=\"  b  a  c    \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          jsx: `() => <img options=\"  b  a  c  a  \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          jsxOutput: `() => <img options=\"  b  a  c    \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          svelte: `<img options=\"  b  a  c  a  \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          svelteOutput: `<img options=\"  b  a  c    \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" />`,\n          vue: `<template><img options=\"  b  a  c  a  \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" /></template>`,\n          vueOutput: `<template><img options=\"  b  a  c    \" settings=\"  b  a  c  a  \" class=\"  b  a  c  a  \" /></template>`,\n\n          errors: 1,\n          options: [{ attributes: [\"options\"] }],\n          settings: { \"better-tailwindcss\": { attributes: [\"settings\"] } }\n        }\n      ]\n    });\n  });\n\n});\n"
  },
  {
    "path": "tests/utils/context.ts",
    "content": "import { dirname } from \"node:path\";\nimport { cwd } from \"node:process\";\n\nimport { getTailwindCSSVersion } from \"better-tailwindcss:tests/utils/version.js\";\nimport { resolveJson } from \"better-tailwindcss:utils/resolvers.js\";\n\nimport type { Context } from \"better-tailwindcss:types/rule.js\";\n\n\nexport function createTestContext(): Context {\n  const installationPath = resolveJson(\"tailwindcss/package.json\", cwd());\n\n  if(!installationPath){\n    throw new Error(\"Tailwind CSS is not installed.\");\n  }\n\n  return {\n    cwd: cwd(),\n    installation: dirname(installationPath),\n    options: {},\n    version: getTailwindCSSVersion()\n  } as Context;\n}\n"
  },
  {
    "path": "tests/utils/eslint.ts",
    "content": "import { ESLint } from \"eslint\";\n\nimport type { Linter } from \"eslint\";\n\n\nexport async function eslint(\n  content: string,\n  config: Linter.Config[]\n): Promise<string> {\n  const eslint = new ESLint({\n    fix: true,\n    overrideConfig: config\n  });\n\n  const [result] = await eslint.lintText(content);\n\n  return result.output ?? content;\n}\n"
  },
  {
    "path": "tests/utils/lint.ts",
    "content": "import { readdirSync } from \"node:fs\";\nimport { normalize } from \"node:path\";\n\nimport eslintParserAngular from \"@angular-eslint/template-parser\";\nimport css from \"@eslint/css\";\nimport eslintParserHTML from \"@html-eslint/parser\";\nimport eslintParserAstro from \"astro-eslint-parser\";\nimport { RuleTester as ESLintRuleTester } from \"eslint\";\nimport { RuleTester as OxlintRuleTester } from \"oxlint/plugins-dev\";\nimport eslintParserSvelte from \"svelte-eslint-parser\";\nimport { tailwind4 } from \"tailwind-csstree\";\nimport eslintParserVue from \"vue-eslint-parser\";\n\nimport { TestDirectory } from \"better-tailwindcss:tests/utils/tmp.js\";\nimport { getNodeVersion } from \"better-tailwindcss:tests/utils/version.js\";\nimport { clearCache } from \"better-tailwindcss:utils/cache.js\";\n\nimport type { ESLint } from \"eslint\";\nimport type { Node as ESNode } from \"estree\";\n\nimport type { CommonOptions } from \"better-tailwindcss:options/descriptions.js\";\nimport type { Context, ESLintRule } from \"better-tailwindcss:types/rule.js\";\n\n\nexport const TEST_SYNTAXES = {\n  angular: {\n    languageOptions: { parser: eslintParserAngular }\n  },\n  astro: {\n    languageOptions: { parser: eslintParserAstro }\n  },\n  css: {\n    language: \"css/css\",\n    languageOptions: {\n      customSyntax: tailwind4,\n      tolerant: true\n    },\n    plugins: { css: css as unknown as ESLint.Plugin }\n  },\n  html: {\n    languageOptions: { parser: eslintParserHTML }\n  },\n  jsx: {\n    languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } }\n  },\n  svelte: {\n    languageOptions: { parser: eslintParserSvelte }\n  },\n  vue: {\n    languageOptions: { parser: eslintParserVue }\n  }\n} as const;\n\ntype Syntaxes = typeof TEST_SYNTAXES;\n\nconst LINTERS = {\n  eslint: {\n    RuleTester: ESLintRuleTester,\n    syntaxes: {\n      angular: TEST_SYNTAXES.angular,\n      astro: TEST_SYNTAXES.astro,\n      css: TEST_SYNTAXES.css,\n      html: TEST_SYNTAXES.html,\n      jsx: TEST_SYNTAXES.jsx,\n      svelte: TEST_SYNTAXES.svelte,\n      vue: TEST_SYNTAXES.vue\n    }\n  },\n  ...getNodeVersion().major >= 22 && {\n    oxlint: {\n      RuleTester: OxlintRuleTester,\n      syntaxes: {\n        jsx: TEST_SYNTAXES.jsx\n      }\n    }\n  }\n} as const;\n\nexport function lint<const Rule extends ESLintRule>(\n  eslintRule: Rule,\n  tests: {\n    invalid?: (\n      {\n        [Key in keyof Syntaxes]?: string;\n      } & {\n        [Key in keyof Syntaxes as `${Key & string}Output`]?: string;\n      } & {\n        errors: { message: string; }[] | number;\n      } & {\n        files?: Record<string, string>;\n        options?: [Partial<CommonOptions & Context<Rule>[\"options\"]>];\n        settings?: Record<string, Partial<CommonOptions>>;\n      }\n    )[];\n    valid?: (\n      {\n        [Key in keyof Syntaxes]?: string;\n      } & {\n        files?: Record<string, string>;\n        options?: [Partial<CommonOptions & Context<Rule>[\"options\"]>];\n        settings?: Record<string, Partial<CommonOptions>>;\n      }\n    )[];\n  }\n) {\n\n  for(const invalid of tests.invalid ?? []){\n\n    clearCache();\n\n    using _ = new TestDirectory(invalid.files);\n\n    for(const { RuleTester, syntaxes } of Object.values(LINTERS)){\n      for(const [name, options] of Object.entries(syntaxes)){\n\n        const ruleTester = new RuleTester(options) as ESLintRuleTester;\n\n        if(!invalid[name]){\n          continue;\n        }\n\n        ruleTester.run(eslintRule.name, eslintRule.rule, {\n          invalid: [{\n            code: invalid[name],\n            errors: invalid.errors,\n            options: invalid.options ?? [],\n            output: invalid[`${name}Output`] ?? null,\n            settings: invalid.settings ?? {}\n          }],\n          valid: []\n        });\n      }\n    }\n  }\n\n  for(const valid of tests.valid ?? []){\n\n    clearCache();\n\n    using _ = new TestDirectory(valid.files);\n\n    for(const { RuleTester, syntaxes } of Object.values(LINTERS)){\n      for(const [name, options] of Object.entries(syntaxes)){\n\n        const ruleTester = new RuleTester(options) as ESLintRuleTester;\n\n        if(!valid[name]){\n          continue;\n        }\n\n        ruleTester.run(eslintRule.name, eslintRule.rule, {\n          invalid: [],\n          valid: [{\n            code: valid[name],\n            options: valid.options ?? [],\n            settings: valid.settings ?? {}\n          }]\n        });\n      }\n\n    }\n  }\n\n}\n\ntype GuardedType<Type> = Type extends (value: any) => value is infer ResultType ? ResultType : never;\n\nexport function findNode<Matcher extends (node: unknown) => node is any>(node: unknown, matcherFunction: Matcher): GuardedType<Matcher> | undefined {\n  if(!node || typeof node !== \"object\"){\n    return;\n  }\n\n  for(const key in node){\n    const value = node[key];\n\n    if(!value || typeof value !== \"object\" || key === \"parent\"){\n      continue;\n    }\n\n    if(matcherFunction(value)){\n      return value;\n    }\n\n    const foundNode = findNode(value, matcherFunction);\n\n    if(foundNode){\n      return foundNode;\n    }\n  }\n}\n\nexport function withParentNodeExtension(node: ESNode, parent: ESNode = node) {\n  for(const key in node){\n    if(typeof node[key] === \"object\" && key !== \"parent\"){\n      if(Array.isArray(node[key])){\n        for(const element of node[key]){\n          element.parent = parent;\n          withParentNodeExtension(element);\n        }\n      } else {\n        node[key].parent = parent;\n        withParentNodeExtension(node[key]);\n      }\n    }\n  }\n  return node;\n}\n\nexport function getFilesInDirectory(importURL: string) {\n  const path = normalize(importURL);\n  const files = readdirSync(path);\n\n  return files.filter(file => !file.includes(\".test.ts\"));\n}\n"
  },
  {
    "path": "tests/utils/prettier.ts",
    "content": "import { format as prettierFormat } from \"prettier\";\n\nimport type { Options } from \"prettier\";\n\n\nexport async function prettier(\n  content: string,\n  options?: Options\n): Promise<string> {\n  return prettierFormat(content, options);\n}\n"
  },
  {
    "path": "tests/utils/setup.ts",
    "content": "import { cleanUpTestDirectory } from \"better-tailwindcss:tests/utils/tmp.js\";\n\n\nexport function teardown() {\n  cleanUpTestDirectory();\n}\n"
  },
  {
    "path": "tests/utils/template.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { ts } from \"./template\";\n\n\ndescribe(\"template utils\", () => {\n\n  it(\"should inject variables correctly\", () => {\n    const vars = \"test\";\n    const test = ts`const test = \"${vars}\";`;\n    expect(test).toBe(\"const test = \\\"test\\\";\");\n  });\n\n  it(\"should remove common white spaces from tagged template literals\", () => {\n    const test = ts`\n      const test = \"test\";\n    `;\n    expect(test).toBe(\"const test = \\\"test\\\";\");\n  });\n\n});\n"
  },
  {
    "path": "tests/utils/template.ts",
    "content": "import { getCachedRegex } from \"better-tailwindcss:utils/regex.js\";\n\n\nconst EOL = \"\\n\";\nconst TABS_IN_SPACES = 4;\n\nexport const ts = createTemplateTag(undefined, true);\nexport const css = createTemplateTag(undefined, true);\nexport const vue = createTemplateTag(undefined, true);\nexport const html = createTemplateTag(undefined, true);\nexport const svelte = createTemplateTag(undefined, true);\nexport const angular = createTemplateTag(undefined, true);\nexport const jsx = createTemplateTag(undefined, true);\n\nexport const dedent = createTemplateTag(4, false);\n\nfunction createTemplateTag(indentation?: number, removeNewLines: boolean = false) {\n  return (templateStrings: TemplateStringsArray, ...values: (boolean | number | string)[]) => {\n    const assembledTemplateString = assembleTemplateString(templateStrings, ...values);\n\n    const contentWithoutSurroundingNewLines = removeNewLines\n      ? removeSurroundingNewLines(assembledTemplateString)\n      : assembledTemplateString;\n\n    const minIndentation = indentation ?? findCommonIndentation(contentWithoutSurroundingNewLines);\n\n    return removeCommonIndentation(contentWithoutSurroundingNewLines, minIndentation);\n  };\n}\n\nfunction findCommonIndentation(content: string, eol: string = EOL) {\n\n  const lines = content.split(eol).filter(line => /\\S/.test(line));\n\n  for(const line of lines){\n    if(getCachedRegex(/^\\S+/).test(line)){\n      return 0;\n    }\n  }\n\n  const spaces = lines.map(\n    line => getCachedRegex(/^[^\\S\\t\\n\\r]+\\S/).test(line)\n      ? line.match(getCachedRegex(/^[^\\S\\t\\n\\r]*/))?.[0].length ?? 0\n      : undefined\n  ).filter(space => space !== undefined);\n\n  const tabs = lines.map(\n    line => getCachedRegex(/^\\t+\\S/).test(line)\n      ? line.match(getCachedRegex(/^\\t*/))?.[0].length ?? 0\n      : undefined\n  ).filter(tab => tab !== undefined);\n\n\n  const tabsInSpaces = tabs.map(tab => tab * TABS_IN_SPACES);\n  const indentations = [...spaces, ...tabsInSpaces] as number[];\n\n  if(indentations.length <= 0){\n    return 0;\n  }\n\n  const minIndentation = Math.min(...indentations);\n\n  return minIndentation - minIndentation % 2;\n\n}\n\nfunction removeCommonIndentation(content: string, minIndentation: number, eol: string = EOL) {\n\n  if(minIndentation <= 0){\n    return content;\n  }\n\n  const lines = content.split(eol);\n\n  return lines.map(line => {\n\n    let spacesLeft = minIndentation;\n\n    for(let i = 0; i < line.length; i++){\n      if(line[i] === \" \"){\n        spacesLeft--;\n      } else if(line[i] === \"\\t\"){\n        spacesLeft -= TABS_IN_SPACES;\n      } else {\n        break;\n      }\n      if(spacesLeft <= 0){\n        line = line.slice(i + 1);\n        break;\n      }\n    }\n\n    return line;\n\n  }).join(eol);\n\n}\n\nfunction removeSurroundingNewLines(content: string) {\n  return content.replace(getCachedRegex(/^\\n|\\n[\\t ]*$/g), \"\");\n}\n\nfunction assembleTemplateString(templateString: TemplateStringsArray, ...values: (boolean | number | string)[]) {\n  return templateString.reduce((acc, str, i) => `${acc}${str}${values[i] ?? \"\"}`, \"\");\n}\n"
  },
  {
    "path": "tests/utils/tmp.ts",
    "content": "import { mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { chdir } from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\n\nimport eslintPluginBetterTailwindCSS from \"better-tailwindcss:configs/config.js\";\n\n\ntype TestDirectoryFiles<Files extends Record<string, string>> = {\n  [File in keyof Files]: { content: Files[File]; path: File; };\n};\n\nexport class TestDirectory<Files extends Record<string, string> = Record<string, string>> {\n\n  private cwd = process.cwd();\n  private storage: Files | undefined;\n\n  public readonly directory: string;\n\n  constructor(files?: Files, isolated: boolean = false) {\n    const randomDir = Math.random()\n      .toString(36)\n      .substring(2, 6);\n\n    this.directory = resolve(getTestDirectoryBasePath(isolated), randomDir);\n\n    mkdirSync(this.directory, { recursive: true });\n\n    chdir(this.directory);\n\n    if(files){\n      this.storage = files;\n\n      for(const name in files){\n        mkdirSync(join(this.directory, dirname(name)), { recursive: true });\n        writeFileSync(join(this.directory, name), files[name]);\n      }\n    }\n\n  }\n\n  public get files(): TestDirectoryFiles<Files> {\n    const storage = this.storage;\n    const directory = this.directory;\n\n    return new Proxy({} as TestDirectoryFiles<Files>, {\n      get(_, key) {\n        if(typeof key !== \"string\"){\n          return;\n        }\n        return { content: storage?.[key], path: join(directory, key) };\n      }\n    });\n  }\n\n  public cleanUp() {\n    chdir(this.cwd);\n    cleanUpTestDirectory(this.directory);\n  }\n\n  [Symbol.dispose]() {\n    this.cleanUp();\n  }\n\n}\n\nfunction getProjectRoot() {\n  return resolve(dirname(fileURLToPath(import.meta.url)), \"../../\");\n}\n\nfunction getTestDirectoryBasePath(isolated: boolean = false) {\n  return resolve(\n    isolated\n      ? tmpdir()\n      : join(getProjectRoot(), \"tmp\"),\n    eslintPluginBetterTailwindCSS.meta.name\n  );\n}\n\nexport function cleanUpTestDirectory(path?: string) {\n  const isolatedBasePath = getTestDirectoryBasePath(true);\n  const basePath = getTestDirectoryBasePath();\n\n  if(path){\n    const resolvedPath = resolve(path);\n    if(!isAbsolute(resolvedPath) || !resolvedPath.startsWith(isolatedBasePath) && !resolvedPath.startsWith(basePath)){\n      throw new Error(`Invalid path: ${path}`);\n    }\n\n    rmSync(path, { force: true, recursive: true });\n  } else {\n    rmSync(isolatedBasePath, { force: true, recursive: true });\n    rmSync(basePath, { force: true, recursive: true });\n  }\n\n}\n"
  },
  {
    "path": "tests/utils/values.ts",
    "content": "export function values(message: string, values: Record<string, string>): string {\n  return Object.entries(values).reduce((msg, [key, value]) => {\n    return msg.replace(new RegExp(`{{\\\\s*${key}\\\\s*}}`, \"g\"), value);\n  }, message);\n}\n"
  },
  {
    "path": "tests/utils/version.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport { cwd } from \"node:process\";\n\nimport { resolveJson } from \"better-tailwindcss:utils/resolvers.js\";\nimport { parseSemanticVersion } from \"better-tailwindcss:utils/version.js\";\n\n\nexport function getTailwindCSSVersion() {\n  const packageJsonPath = resolveJson(\"tailwindcss/package.json\", cwd());\n\n  if(!packageJsonPath){\n    throw new Error(\"Tailwind CSS is not installed.\");\n  }\n\n  const packageJson = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n  return parseSemanticVersion(packageJson.version);\n}\n\nexport function getNodeVersion() {\n  return parseSemanticVersion(process.versions.node);\n}\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"nodenext\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"noEmit\": false,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"target\": \"ES2020\"\n  },\n  \"include\": [\n    \"src/tailwindcss/**/*.ts\",\n    \"src/configs/config.ts\",\n    \"src/api/defaults.ts\",\n    \"src/api/types.ts\"\n  ],\n  \"exclude\": [\n    \"**/*.test.ts\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@schoero/configs/tsconfig\",\n  \"compilerOptions\": {\n\n    // paths\n    \"baseUrl\": \".\",\n    \"lib\": [\"ESNext\"],\n    \"module\": \"Preserve\",\n    \"paths\": {\n      \"better-tailwindcss:build/*\": [\"build/*\"],\n      \"better-tailwindcss:options/*\": [\"src/options/*\"],\n      \"better-tailwindcss:configs/*\": [\"src/configs/*\"],\n      \"better-tailwindcss:parsers/*\": [\"src/parsers/*\"],\n      \"better-tailwindcss:rules/*\": [\"src/rules/*\"],\n      \"better-tailwindcss:tests/*\": [\"tests/*\"],\n      \"better-tailwindcss:types/*\": [\"src/types/*\"],\n      \"better-tailwindcss:utils/*\": [\"src/utils/*\",\"src/async-utils/*\"],\n      \"better-tailwindcss:tailwindcss/*\": [\"src/tailwindcss/*\"]\n    },\n\n    // general\n    \"noEmit\": true,\n    \"target\": \"ES2020\",\n\n    // strictness\n    \"exactOptionalPropertyTypes\": true,\n    \"noImplicitAny\": false,\n\n    // imports\n    \"verbatimModuleSyntax\": true\n  }\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { config } from \"@schoero/configs/vite\";\nimport { defineConfig } from \"vitest/config\";\n\n\nexport default defineConfig({\n  ...config,\n  test: {\n    disableConsoleIntercept: true,\n    fileParallelism: false,\n    globalSetup: \"./tests/utils/setup.ts\",\n    testTimeout: 10_000\n  }\n});\n"
  }
]