├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── cr.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── README_zh.md ├── assets ├── demo.gif └── kv.png ├── eslint.config.mjs ├── icon.png ├── media └── main.js ├── package.json ├── patches ├── @vue__compiler-core.patch └── @vue__compiler-sfc.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── index.ts ├── openDocumentation.ts ├── openPlayground.ts ├── parser.ts ├── process.ts ├── transform.ts ├── type.ts └── utils │ └── index.ts ├── test ├── index.test.ts └── transformUno.test.ts ├── tsconfig.json ├── tsdown.config.ts └── tsup.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Simon-He95 2 | custom: ['https://github.com/Simon-He95/sponsor'] 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | merge_group: {} 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | 25 | - name: Set node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 18.18.2 29 | cache: pnpm 30 | 31 | - name: Setup 32 | run: npm i -g @antfu/ni 33 | 34 | - name: Install 35 | run: nci 36 | 37 | - name: Lint 38 | run: nr lint 39 | 40 | test: 41 | runs-on: ${{ matrix.os }} 42 | 43 | strategy: 44 | matrix: 45 | os: [ubuntu-latest] 46 | node_version: [18.18.2, lts/*] 47 | include: 48 | - os: macos-latest 49 | node_version: lts/* 50 | - os: windows-latest 51 | node_version: lts/* 52 | fail-fast: false 53 | 54 | steps: 55 | - name: Set git to use LF 56 | run: | 57 | git config --global core.autocrlf false 58 | git config --global core.eol lf 59 | 60 | - uses: actions/checkout@v4 61 | 62 | - name: Install pnpm 63 | uses: pnpm/action-setup@v4 64 | with: 65 | version: 9 66 | 67 | - name: Set node ${{ matrix.node_version }} 68 | uses: actions/setup-node@v4 69 | with: 70 | node-version: ${{ matrix.node_version }} 71 | cache: pnpm 72 | 73 | - run: corepack enable 74 | 75 | - name: Setup 76 | run: npm i -g @antfu/ni 77 | 78 | - name: Install 79 | run: nci 80 | 81 | - name: Build 82 | run: nr build 83 | 84 | - name: Test 85 | run: nr test 86 | -------------------------------------------------------------------------------- /.github/workflows/cr.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | 3 | permissions: 4 | contents: read 5 | pull-requests: write 6 | 7 | on: 8 | pull_request: 9 | types: [opened, reopened, synchronize] 10 | 11 | jobs: 12 | test: 13 | if: ${{ contains(github.event.*.labels.*.name, 'gpt review') }} # Optional; to run only when a label is attached 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: anc95/ChatGPT-CodeReview@main 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 20 | # Optional 21 | LANGUAGE: Chinese 22 | MODEL: 23 | top_p: 1 24 | temperature: 1 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | - run: pnpm dlx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | 29 | # # Uncomment the following lines to publish to npm on CI 30 | # 31 | # - run: pnpm install 32 | # - run: pnpm publish -r --access public 33 | # env: 34 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | # NPM_CONFIG_PROVENANCE: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | *.vsix 7 | coverage 8 | dist 9 | lib-cov 10 | logs 11 | node_modules 12 | temp 13 | .eslintcache 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | node-linker=hoisted 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "amodio.tsl-problem-matcher" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/**/*.js" 14 | ], 15 | "preLaunchTask": "npm: dev" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.inlineSuggest.showToolbar": "onHover" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "dev", 9 | "isBackground": true, 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "problemMatcher": [ 14 | { 15 | "base": "$tsc-watch", 16 | "background": { 17 | "activeOnStart": true, 18 | "beginsPattern": "tsdown config", 19 | "endsPattern": "Build complete" 20 | } 21 | } 22 | ], 23 | "group": "build" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # node_modules 2 | .github 3 | .vscode 4 | assets 5 | test 6 | src 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Simon He 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | UnoT 3 |

4 |

English | 简体中文

5 | 6 | UnoT is short for unocss tools, to provide a better development experience using unocss in vscode. It integrated [tounocss](https://github.com/Simon-He95/tounocss)、 [vscode uno magic](https://github.com/Simon-He95/vscode-uno-magic) 7 | 8 | ![demo](/assets/demo.gif) 9 | 10 | ## 🦸🏻 Ability 11 | 12 | - Provides hover style prompt css code corresponding to UnoCss 13 | - Turn on uno-magic to provide the ability to automatically process spaces and brackets 14 | - Provides shortcut keys to automatically convert the style copied by the browser to UnoCss 15 | - Right-click provides a website to quickly open UnoCss documents and Unot online edited conversion results 16 | 17 | ## 💡 Open uno-magic 18 | - text-\[red,hover:pink,2xl,lg:hover:3xl\] -> text-red hover:text-pink text-2xl lg:hover:text-3xl 19 | - class or className content like `w-calc(100% - 20px)` -> `w-[calc(100%-20px)]` [🔎detail](https://github.com/Simon-He95/vscode-uno-magic) 20 | - Provides code selection for unocss syntax [🔎detail](https://github.com/Simon-He95/vscode-uno-magic) 21 | - Provide the unocss hover to display the css code [🔎detail](https://github.com/Simon-He95/unocss-to-css) 22 | - bg#fff -> bg-#fff 23 | - maxw-100% -> max-w-[100%] 24 | - bg-[rgba(255, 255, 255, 0.5)] -> bg-[rgba(255,255,255,0.5)] 25 | - translatex--50% -> translate-x-[-50%] 26 | - hover:(text-red bg-blue) -> hover:text-red hover:bg-blue 27 | - !(text-red bg-blue) -> !text-red !bg-blue 28 | - h="[calc(100% - 20px)]" -> h="[calc(100%-20px)]" 29 | 30 | ## Powered by 31 | - [transformToUnoCSS](https://github.com/Simon-He95/transformToUnoCSS) 32 | - [transform-to-tailwindcss-core](https://github.com/Simon-He95/transform-to-tailwindcss-core) 33 | 34 | ## Feature 35 | Support css in the design draft directly through the shortcut key `Mac`? `cmd+alt+v` : `ctrl+alt+v` is automatically converted to unocss, and will be automatically processed into in-line unocss format or class form according to your location. 36 | 37 | ## Configuration 38 | - You can use config to control some matching rules, such as strict-splicing, or the generated calculation result is `-[10px]` or `-10px` 39 | 40 | ``` json 41 | { 42 | "unot.classMode": { 43 | "type": "boolean", 44 | "default": true, 45 | "description": "Enable/disable class mode" 46 | }, 47 | "unot.variantGroup": { 48 | "type": "boolean", 49 | "default": true, 50 | "description": "Enable/disable transform hover:(x1 x2) to hover:x1 hover:x2" 51 | }, 52 | "unot.strictVariable": { 53 | "type": "boolean", 54 | "default": true, 55 | "description": "if true w10px or w-10px will transform w-[10px]" 56 | }, 57 | "unot.strictHyphen": { 58 | "type": "boolean", 59 | "default": false, 60 | "description": "if true bg#fff or bgrgba(0,0,0,.0) will transform bg-[#fff] or bg-[rgba(0,0,0,.0)]" 61 | }, 62 | "unot.switchMagic": { 63 | "type": "boolean", 64 | "default": true, 65 | "description": "switch magic" 66 | }, 67 | "unot.useHex": { 68 | "type": "boolean", 69 | "default": false, 70 | "description": "use hex color transform #fff to hex-fff" 71 | }, 72 | "unot.presets": { 73 | "type": "array", 74 | "default": [], 75 | "description": "unocss transform presets" 76 | }, 77 | "unot.dark": { 78 | "type": "object", 79 | "default": {}, 80 | "description": "unocss dark theme style" 81 | }, 82 | "unot.light": { 83 | "type": "object", 84 | "default": {}, 85 | "description": "unocss light theme style" 86 | } 87 | } 88 | ``` 89 | 90 | ## :coffee: 91 | 92 | [buy me a cup of coffee](https://github.com/Simon-He95/sponsor) 93 | 94 | ## License 95 | 96 | [MIT](./license) 97 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |

2 | UnoT 3 |

4 |

English | 简体中文

5 | 6 | UnoT 是 unocss tools 的简写,它是为了在vscode中使用unocss提供更加好的开发体验. 它集成了 [tounocss](https://github.com/Simon-He95/tounocss), [vscode uno magic](https://github.com/Simon-He95/vscode-uno-magic) 7 | 8 | ![demo](/assets/demo.gif) 9 | 10 | ## 🦸🏻 能力 11 | - 提供了hover style 提示对应 UnoCss 的css代码 12 | - 开启 uno-magic 提供自动处理空格和括号的能力 13 | - 提供了快捷键自动将浏览器复制的样式转换成 UnoCss 14 | - 右键提供了快速打开 UnoCss 的文档 和 Unot 在线编辑的转换结果网站 15 | 16 | ## 💡 开启uno-magic 17 | - text-\[red,hover:pink,2xl,lg:hover:3xl\] -> text-red hover:text-pink text-2xl lg:hover:text-3xl 18 | - class or className content like `w-calc(100% - 20px)` -> `w-[calc(100%-20px)]` [🔎详情](https://github.com/Simon-He95/vscode-uno-magic) 19 | - Provides code selection for unocss syntax [🔎详情](https://github.com/Simon-He95/vscode-uno-magic) 20 | - Provide the unocss hover to display the css code [🔎详情](https://github.com/Simon-He95/unocss-to-css) 21 | - bg#fff -> bg-#fff 22 | - maxw-100% -> max-w-[100%] 23 | - bg-[rgba(255, 255, 255, 0.5)] -> bg-[rgba(255,255,255,0.5)] 24 | - -translatex50% -> translate-x-[-50%] 25 | - hover:(text-red bg-blue) -> hover:text-red hover:bg-blue 26 | - !(text-red bg-blue) -> !text-red !bg-blue 27 | - h="[calc(100% - 20px)]" -> h="[calc(100%-20px)]" 28 | 29 | ## 核心能力来源于 30 | - [transformToUnoCSS](https://github.com/Simon-He95/transformToUnoCSS) 31 | - [transform-to-tailwindcss-core](https://github.com/Simon-He95/transform-to-tailwindcss-core) 32 | 33 | ## 新特性 34 | 支持将设计稿中的css直接通过快捷键 `Mac` ? `cmd+alt+v` : `ctrl+alt+v` 自动转换成unocss,并且会根据你的位置自动处理成行内的unocss格式还是class形式的 35 | 36 | ## 参数配置 37 | - 您可以使用配置来控制一些匹配规则,例如严格拆分,或者生成的计算结果是`-[10px]`或`-10px` 38 | 39 | ``` json 40 | { 41 | "unot.classMode": { 42 | "type": "boolean", 43 | "default": true, 44 | "description": "Enable/disable class mode" 45 | }, 46 | "unot.variantGroup": { 47 | "type": "boolean", 48 | "default": true, 49 | "description": "Enable/disable transform hover:(x1 x2) to hover:x1 hover:x2" 50 | }, 51 | "unot.strictVariable": { 52 | "type": "boolean", 53 | "default": true, 54 | "description": "if true w10px or w-10px will transform w-[10px]" 55 | }, 56 | "unot.strictHyphen": { 57 | "type": "boolean", 58 | "default": false, 59 | "description": "if true bg#fff or bgrgba(0,0,0,.0) will transform bg-[#fff] or bg-[rgba(0,0,0,.0)]" 60 | }, 61 | "unot.switchMagic": { 62 | "type": "boolean", 63 | "default": true, 64 | "description": "switch magic" 65 | }, 66 | "unot.useHex": { 67 | "type": "boolean", 68 | "default": false, 69 | "description": "use hex color transform #fff to hex-fff" 70 | }, 71 | "unot.presets": { 72 | "type": "array", 73 | "default": [], 74 | "description": "unocss transform presets" 75 | }, 76 | "unot.dark": { 77 | "type": "object", 78 | "default": {}, 79 | "description": "unocss dark theme style" 80 | }, 81 | "unot.light": { 82 | "type": "object", 83 | "default": {}, 84 | "description": "unocss light theme style" 85 | } 86 | } 87 | ``` 88 | 89 | ## :coffee: 90 | 91 | [请我喝一杯咖啡](https://github.com/Simon-He95/sponsor) 92 | 93 | ## License 94 | 95 | [MIT](./license) 96 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simon-He95/unot/d1409fabed55572dc98b7455858cb3a5d941117e/assets/demo.gif -------------------------------------------------------------------------------- /assets/kv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simon-He95/unot/d1409fabed55572dc98b7455858cb3a5d941117e/assets/kv.png -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu( 5 | { 6 | ignores: [ 7 | // eslint ignore globs here 8 | 'media', 9 | ], 10 | }, 11 | { 12 | rules: { 13 | // overrides 14 | 'ts/no-var-requires': 'off', 15 | 'ts/no-require-imports': 'off', 16 | 'style/max-statements-per-line': 'off', 17 | 'import/no-mutable-exports': 'off', 18 | 'no-console': 'off', 19 | 'unused-imports/no-unused-vars': 'off', 20 | 'regexp/no-empty-alternative': 'off', 21 | 'regexp/no-super-linear-backtracking': 'off', 22 | 'regexp/no-dupe-disjunctions': 'off', 23 | 'perfectionist/sort-imports': 'off', 24 | }, 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simon-He95/unot/d1409fabed55572dc98b7455858cb3a5d941117e/icon.png -------------------------------------------------------------------------------- /media/main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const vscode = acquireVsCodeApi() 3 | window.addEventListener('message', (e) => { 4 | const data = e.data 5 | if (data) { 6 | if (data.eventType === 'copy') 7 | vscode.postMessage({ type: 'copy', data: data.text }) 8 | } 9 | }) 10 | }()) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "simonhe", 3 | "name": "unot", 4 | "displayName": "UnoT - UnoCSS Tools", 5 | "version": "0.1.12", 6 | "packageManager": "pnpm@10.12.2", 7 | "description": "UnoCSS Tools: VS Code extension for UnoCSS, CSS utility class autocomplete, intellisense, and style conversion.", 8 | "author": "Simon He ", 9 | "license": "MIT", 10 | "funding": "https://github.com/sponsors/Simon-He95", 11 | "homepage": "https://github.com/Simon-He95/unot#readme", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Simon-He95/unot" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/Simon-He95/unot/issues" 18 | }, 19 | "sponsor": { 20 | "url": "https://github.com/Simon-He95/sponsor" 21 | }, 22 | "keywords": [ 23 | "unocss", 24 | "css", 25 | "intellisense", 26 | "autocomplete", 27 | "vscode", 28 | "utility", 29 | "tailwind", 30 | "style", 31 | "converter", 32 | "transform", 33 | "web", 34 | "frontend", 35 | "extension", 36 | "editor", 37 | "snippets" 38 | ], 39 | "categories": [ 40 | "Linters", 41 | "Snippets", 42 | "Other" 43 | ], 44 | "main": "./dist/index.js", 45 | "icon": "icon.png", 46 | "engines": { 47 | "vscode": "^1.77.0" 48 | }, 49 | "activationEvents": [ 50 | "onStartupFinished" 51 | ], 52 | "contributes": { 53 | "submenus": [ 54 | { 55 | "id": "unot", 56 | "label": "unot" 57 | } 58 | ], 59 | "menus": { 60 | "editor/context": [ 61 | { 62 | "submenu": "unot", 63 | "group": "6_px" 64 | } 65 | ], 66 | "unot": [ 67 | { 68 | "command": "UnoT.ToUnocss", 69 | "when": "editorFocus && !editorHasSelection && (editorLangId == javascript || editorLangId == typescript || editorLangId == vue)" 70 | }, 71 | { 72 | "command": "UnoT.InlineStyleToUnocss", 73 | "when": "editorHasSelection" 74 | }, 75 | { 76 | "command": "UnoT.openDocumentation" 77 | }, 78 | { 79 | "command": "UnoT.openPlayground" 80 | } 81 | ] 82 | }, 83 | "commands": [ 84 | { 85 | "command": "UnoT.ToUnocss", 86 | "title": "page -> Unocss" 87 | }, 88 | { 89 | "command": "UnoT.InlineStyleToUnocss", 90 | "title": "style -> Unocss" 91 | }, 92 | { 93 | "command": "UnoT.openDocumentation", 94 | "title": "open unocss documentation" 95 | }, 96 | { 97 | "command": "UnoT.openPlayground", 98 | "title": "open unot playground" 99 | }, 100 | { 101 | "command": "UnoT.transform", 102 | "title": "transform copied css to unocss" 103 | } 104 | ], 105 | "configuration": { 106 | "type": "object", 107 | "title": "unot", 108 | "properties": { 109 | "unot.classMode": { 110 | "type": "boolean", 111 | "default": true, 112 | "description": "Enable/disable class mode" 113 | }, 114 | "unot.variantGroup": { 115 | "type": "boolean", 116 | "default": true, 117 | "description": "Enable/disable transform hover:(x1 x2) to hover:x1 hover:x2" 118 | }, 119 | "unot.strictVariable": { 120 | "type": "boolean", 121 | "default": true, 122 | "description": "if true w10px or w-10px will transform w-[10px]" 123 | }, 124 | "unot.strictHyphen": { 125 | "type": "boolean", 126 | "default": false, 127 | "description": "if true bg#fff or bgrgba(0,0,0,.0) will transform bg-[#fff] or bg-[rgba(0,0,0,.0)]" 128 | }, 129 | "unot.switchMagic": { 130 | "type": "boolean", 131 | "default": true, 132 | "description": "switch magic" 133 | }, 134 | "unot.useHex": { 135 | "type": "boolean", 136 | "default": false, 137 | "description": "use hex color transform #fff to hex-fff" 138 | }, 139 | "unot.presets": { 140 | "type": "array", 141 | "default": [], 142 | "description": "unocss transform presets" 143 | }, 144 | "unot.dark": { 145 | "type": "object", 146 | "default": {}, 147 | "description": "unocss dark theme style" 148 | }, 149 | "unot.light": { 150 | "type": "object", 151 | "default": {}, 152 | "description": "unocss light theme style" 153 | } 154 | } 155 | }, 156 | "keybindings": [ 157 | { 158 | "command": "UnoT.transform", 159 | "key": "cmd+alt+v", 160 | "when": "isMac" 161 | }, 162 | { 163 | "command": "UnoT.transform", 164 | "key": "ctrl+alt+v", 165 | "when": "!isMac" 166 | } 167 | ] 168 | }, 169 | "scripts": { 170 | "dev": "pnpm build --watch", 171 | "test": "vitest", 172 | "build": "tsdown src/index.ts", 173 | "pack": "vsce package", 174 | "lint": "eslint . --cache", 175 | "lint:fix": "eslint . --fix", 176 | "publish": "pnpx vsce publish", 177 | "typecheck": "tsc --noEmit", 178 | "release": "pnpm run build --minify && bumpp && pnpm publish" 179 | }, 180 | "dependencies": { 181 | "@typescript-eslint/typescript-estree": "^8.34.1", 182 | "@vscode-use/createwebview": "^0.0.13", 183 | "fast-glob": "^3.3.3", 184 | "svelte": "^4.2.20", 185 | "transform-to-unocss": "^0.1.16", 186 | "transform-to-unocss-core": "^0.0.68" 187 | }, 188 | "devDependencies": { 189 | "@antfu/eslint-config": "^3.16.0", 190 | "@types/node": "^18.19.112", 191 | "@types/vscode": "1.77.0", 192 | "@vscode-use/utils": "^0.1.59", 193 | "@vscode/vsce": "^3.5.0", 194 | "@vue/compiler-sfc": "^3.5.17", 195 | "bumpp": "^9.11.1", 196 | "eslint": "^9.29.0", 197 | "find-up": "^7.0.0", 198 | "tsdown": "^0.9.9", 199 | "typescript": "^5.8.3", 200 | "vitest": "^0.29.8" 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /patches/@vue__compiler-core.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/compiler-core.cjs.js b/dist/compiler-core.cjs.js 2 | index 8251431d00b46ada29983a63593cbcec443e75b5..594f66e24692e01701219a80ff86d4fb18bb3e4e 100644 3 | --- a/dist/compiler-core.cjs.js 4 | +++ b/dist/compiler-core.cjs.js 5 | @@ -1063,7 +1063,7 @@ class Tokenizer { 6 | this.buffer = input; 7 | while (this.index < this.buffer.length) { 8 | const c = this.buffer.charCodeAt(this.index); 9 | - if (c === 10) { 10 | + if (c === 10 && this.state !== 33) { 11 | this.newlines.push(this.index); 12 | } 13 | switch (this.state) { 14 | diff --git a/dist/compiler-core.esm-bundler.js b/dist/compiler-core.esm-bundler.js 15 | index 81447fce87543763266bba5a6d668bc7498a494e..47d6c9e31ee62e089a142276ff19d0580da67c72 100644 16 | --- a/dist/compiler-core.esm-bundler.js 17 | +++ b/dist/compiler-core.esm-bundler.js 18 | @@ -1023,7 +1023,7 @@ class Tokenizer { 19 | this.buffer = input; 20 | while (this.index < this.buffer.length) { 21 | const c = this.buffer.charCodeAt(this.index); 22 | - if (c === 10) { 23 | + if (c === 10 && this.state !== 33) { 24 | this.newlines.push(this.index); 25 | } 26 | switch (this.state) { 27 | -------------------------------------------------------------------------------- /patches/@vue__compiler-sfc.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/compiler-sfc.esm-browser.js b/dist/compiler-sfc.esm-browser.js 2 | index bcac8074be4193e3e56f1ec374e15e6e31cb9378..f23878f73144ebd14c5322bb5cfa345ab9481eb5 100644 3 | --- a/dist/compiler-sfc.esm-browser.js 4 | +++ b/dist/compiler-sfc.esm-browser.js 5 | @@ -1921,7 +1921,7 @@ class Tokenizer { 6 | this.buffer = input; 7 | while (this.index < this.buffer.length) { 8 | const c = this.buffer.charCodeAt(this.index); 9 | - if (c === 10) { 10 | + if (c === 10 && this.state !== 33) { 11 | this.newlines.push(this.index); 12 | } 13 | switch (this.state) { 14 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - examples/* 4 | 5 | patchedDependencies: 6 | '@vue/compiler-core': patches/@vue__compiler-core.patch 7 | '@vue/compiler-sfc': patches/@vue__compiler-sfc.patch 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { TextEditorDecorationType } from 'vscode' 2 | import type { ChangeList } from './type' 3 | import { addEventListener, createBottomBar, createHover, createLog, createMarkdownString, createPosition, createRange, createStyle, getActiveText, getActiveTextEditor, getConfiguration, getCopyText, getCurrentFileUrl, getLineText, getLocale, getPosition, getSelection, message, nextTick, registerCommand, registerHoverProvider, saveFile, setConfiguration, setCopyText, updateText } from '@vscode-use/utils' 4 | import { findUp } from 'find-up' 5 | import { toUnocssClass, transformStyleToUnocss } from 'transform-to-unocss-core' 6 | import * as vscode from 'vscode' 7 | import { openDocumentation } from './openDocumentation' 8 | import { openPlayground } from './openPlayground' 9 | import { parser } from './parser' 10 | import { CssToUnocssProcess } from './process' 11 | import { rules, transformAttrs, transformClassAttr } from './transform' 12 | import { getMultipedUnocssText, hasFile, highlight, LRUCache, parserAst } from './utils' 13 | import path from 'node:path' 14 | import { existsSync } from 'node:fs' 15 | 16 | const cacheMap = new LRUCache(5000) 17 | const logger = createLog('Unot') 18 | export let toRemFlag = false 19 | export let decorationType: TextEditorDecorationType 20 | export async function activate(context: vscode.ExtensionContext) { 21 | logger.info('Unot is now active!') 22 | // 注册打开文档事件 23 | openDocumentation(context) 24 | openPlayground(context) 25 | const pkgs = await hasFile(['**/package.json']) 26 | const hasUnoDep = (json: any) => (json.devDependencies && 'unocss' in json.devDependencies) || (json.dependencies && 'unocss' in json.dependencies) 27 | const isNotUnocss = !pkgs.some(pkg => hasUnoDep(JSON.parse(pkg))) 28 | 29 | const styleReg = /style="([^"]+)"/ 30 | const { presets = [], prefix = ['ts', 'js', 'vue', 'tsx', 'jsx', 'svelte'], dark, light } = getConfiguration('unot') 31 | const process = new CssToUnocssProcess() 32 | const LANS = ['html', 'javascriptreact', 'typescript', 'typescriptreact', 'vue', 'svelte', 'solid', 'swan', 'react', 'js', 'ts', 'tsx', 'jsx', 'wxml', 'axml', 'css', 'wxss', 'acss', 'less', 'scss', 'sass', 'stylus', 'wxss', 'acss'] 33 | const md = createMarkdownString() 34 | md.isTrusted = true 35 | md.supportHtml = true 36 | let copyClass = '' 37 | let copyAttr = '' 38 | let copyRange: any 39 | const style = { 40 | dark: Object.assign({ 41 | textDecoration: 'underline', 42 | backgroundColor: 'rgba(144, 238, 144, 0.5)', 43 | color: 'black', 44 | }, dark), 45 | light: Object.assign({ 46 | textDecoration: 'underline', 47 | backgroundColor: 'rgba(255, 165, 0, 0.5)', 48 | color: '#ffffff', 49 | borderRadius: '6px', 50 | }, light), 51 | } 52 | decorationType = createStyle(style) 53 | 54 | // 注册ToUnocss命令 55 | context.subscriptions.push(registerCommand('UnoT.ToUnocss', async () => { 56 | const textEditor = getActiveTextEditor()! 57 | const doc = textEditor.document 58 | const fileName = doc.fileName 59 | // 获取全部文本区域 60 | const selection = createRange([0, 0], [doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length]) 61 | const text = doc.getText(selection) 62 | // 替换文件内容 63 | const newSelection = await process.convertAll(text, fileName) 64 | if (!newSelection) 65 | return 66 | textEditor.edit((builder) => { 67 | builder.replace(selection, newSelection) 68 | }) 69 | })) 70 | 71 | // 注册InlineStyleToUnocss命令 72 | context.subscriptions.push(registerCommand('UnoT.InlineStyleToUnocss', async () => { 73 | const textEditor = getActiveTextEditor()! 74 | const doc = textEditor.document 75 | let selection: vscode.Selection | vscode.Range = textEditor.selection 76 | // 获取选中区域 77 | if (selection.isEmpty) 78 | selection = createRange([0, 0], [doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length]) 79 | 80 | const text = doc.getText(selection) 81 | const newSelection = await process.convert(text) 82 | if (!newSelection) 83 | return 84 | // 替换文件内容 85 | updateText((builder) => { 86 | builder.replace(selection, newSelection) 87 | }) 88 | })) 89 | 90 | // 注册快捷指令 91 | context.subscriptions.push(registerCommand('UnoT.transform', async () => { 92 | const selection = getSelection() 93 | if (!selection) 94 | return 95 | const { line, character, lineText } = selection 96 | const copyText = (await getCopyText()).trim() 97 | if (!copyText) 98 | return 99 | const locale = getLocale() 100 | const isZh = locale.includes('zh') 101 | let pre = character - 1 102 | let prefixName = '' 103 | while (pre > 0 && (lineText[pre] !== '"' || lineText[pre - 1] !== '=') && (lineText[pre] !== '{' || lineText[pre - 1] !== '=')) { 104 | if ((lineText[pre] === '>' || lineText[pre] === '"') && lineText[pre - 1] !== '=') { 105 | prefixName = '' 106 | break 107 | } 108 | pre-- 109 | } 110 | 111 | if (lineText[--pre] === '=') { 112 | pre-- 113 | while (pre > 0 && !(/[\s'"> { 132 | const useHex = getConfiguration('unot.useHex') 133 | 134 | if (useHex) 135 | transferred = transferred.replace(/\[#([a-z0-9]+)\]/gi, 'hex-$1') 136 | 137 | builder.insert(createPosition(line, character), transferred) 138 | }) 139 | 140 | message.info(`${isZh ? '🎉 转换成功:' : '🎉 Successful conversion: '}${transferred}`) 141 | })) 142 | 143 | context.subscriptions.push(registerCommand('UnoT.copyAttr', () => { 144 | setCopyText(copyAttr) 145 | message.info(`copy successfully ➡️ ${copyAttr}`) 146 | replaceStyleToAttr(copyAttr, true) 147 | })) 148 | 149 | context.subscriptions.push(registerCommand('UnoT.copyClass', () => { 150 | setCopyText(copyClass) 151 | message.info(`copy successfully ➡️ ${copyClass}`) 152 | replaceStyleToAttr(copyClass, false) 153 | })) 154 | 155 | function replaceStyleToAttr(text: string, isAttr: boolean) { 156 | let item: any 157 | let isRemoveAfter = false 158 | if (copyRange?.length) { 159 | // 如果最后一位的后面跟着; 则end+1 160 | const afterChar = getLineText(copyRange[0].range.end.line)![copyRange[0].range.end.character] 161 | isRemoveAfter = afterChar === ';' 162 | item = { 163 | range: copyRange[0].range, 164 | } 165 | } 166 | else { 167 | item = { 168 | range: getActiveTextEditor()!.selection, 169 | } 170 | } 171 | 172 | const ast = parser(getActiveText()!, item.range.start) 173 | if (ast?.tag) { 174 | const propClass = ast.props?.find((i: any) => i.name === (ast.isJsx ? 'className' : 'class')) 175 | if (!isAttr && propClass) { 176 | updateText((edit) => { 177 | edit.insert(createPosition(propClass.value.loc.start.line - 1, propClass.value.loc.start.column), propClass.value.content ? `${text} ` : text) 178 | edit.replace(updateRange(item.range), '') 179 | }) 180 | } 181 | else if (ast.props?.length > 1) { 182 | const pos = ast.props.find((i: any) => i.name !== 'style')!.loc 183 | updateText((edit) => { 184 | edit.insert(createPosition(pos.start.line - 1, pos.start.column - 1), isAttr ? `${text} ` : `${ast.isJsx ? 'className' : 'class'}="${text}" `) 185 | edit.replace(updateRange(item.range), '') 186 | }) 187 | } 188 | else { 189 | const pos = { 190 | line: ast.loc.start.line, 191 | column: ast.loc.start.column + ast.tag.length + 1, 192 | offset: ast.loc.start.offset + ast.tag.length + 1, 193 | } 194 | updateText((edit) => { 195 | edit.insert(createPosition(pos.line - 1, pos.column), isAttr ? `${text} ` : `${ast.isJsx ? 'className' : 'class'}="${text}" `) 196 | edit.replace(updateRange(item.range), '') 197 | }) 198 | } 199 | } 200 | else { 201 | updateText((edit) => { 202 | edit.replace(updateRange(item.range), text) 203 | }) 204 | } 205 | 206 | function updateRange(range: any) { 207 | if (!isRemoveAfter) 208 | return range 209 | return createRange([range.start.line, range.start.character], [range.end.line, range.end.character + 1]) 210 | } 211 | } 212 | 213 | if (isNotUnocss) { 214 | logger.error(`current is not unocss project, please install unocss first!`) 215 | return 216 | } 217 | 218 | // style to unocss hover事件 219 | context.subscriptions.push(registerHoverProvider(LANS, (document, position) => { 220 | // 获取当前选中的文本范围 221 | const editor = getActiveTextEditor() 222 | if (!editor) 223 | return 224 | // 移除样式 225 | editor.setDecorations(decorationType, []) 226 | if (isNotUnocss) 227 | return 228 | const selection = editor.selection 229 | const wordRange = new vscode.Range(selection.start, selection.end) 230 | let selectedText = editor.document.getText(wordRange) 231 | const realRangeMap: any = [] 232 | if (!selectedText) { 233 | const range = document.getWordRangeAtPosition(position) as any 234 | if (!range) 235 | return 236 | let word = document.getText(range) 237 | if (!word) 238 | return 239 | const line = range.c.c 240 | const lineNumber = position.line 241 | const lineText = document.lineAt(lineNumber).text 242 | const styleMatch = word.match(styleReg) 243 | if (styleMatch) { 244 | word = styleMatch[1] 245 | const index = styleMatch.index! 246 | realRangeMap.push({ 247 | content: styleMatch[0], 248 | range: createRange([line, index!], [line, index! + styleMatch[1].length]), 249 | 250 | }) 251 | } 252 | else { 253 | // 可能存在多项,查找离range最近的 254 | if (lineText.indexOf(':') < 1) 255 | return 256 | const wholeReg = new RegExp(`([\\w\\-]+\\s*:\\s)?([\\w\\-\\[\\(\\!]+)?${word}(:*\\s*[^:"}{\`;>]+)?`, 'g') 257 | for (const match of lineText.matchAll(wholeReg)) { 258 | const { index } = match 259 | const pos = index! + match[0].indexOf(word) 260 | if (pos === range?.c?.e) { 261 | word = match[0] 262 | realRangeMap.push({ 263 | content: match[0], 264 | range: createRange([line, index!], [line, index! + match[0].length]), 265 | }) 266 | break 267 | } 268 | } 269 | } 270 | selectedText = word.replace(/'/g, '').trim() 271 | } 272 | 273 | // 获取当前选中的文本内容 274 | if (!selectedText || !/[\w\-]+\s*:[^.]+/.test(selectedText)) 275 | return 276 | const key = `${selectedText}-${toRemFlag}` 277 | if (cacheMap.has((key))) 278 | return setStyle(cacheMap.get(key), realRangeMap) 279 | const selectedUnocssText = getMultipedUnocssText(selectedText) 280 | if (!selectedUnocssText) 281 | return 282 | // 设置缓存 283 | cacheMap.set(key, selectedUnocssText) 284 | 285 | return setStyle(selectedUnocssText, realRangeMap) 286 | })) 287 | 288 | context.subscriptions.push(addEventListener('text-visible-change', () => { 289 | // 移除装饰器 290 | getActiveTextEditor()?.setDecorations(decorationType, []) 291 | })) 292 | 293 | context.subscriptions.push(addEventListener('text-change', ({ reason, contentChanges, document }) => { 294 | if (document.languageId === 'Log' || !contentChanges.length) 295 | return 296 | getActiveTextEditor()?.setDecorations(decorationType, []) 297 | })) 298 | 299 | context.subscriptions.push(addEventListener('selection-change', () => getActiveTextEditor()?.setDecorations(decorationType, []))) 300 | 301 | function setStyle(selectedUnocssText: string, rangeMap: vscode.Range[]) { 302 | // 增加decorationType样式 303 | md.value = '' 304 | copyAttr = selectedUnocssText 305 | copyClass = selectedUnocssText 306 | .replace(/([^\s=]+)="([^"]+)"/g, (_, v1, v2) => 307 | v2.split(' ').map((v: string) => `${v1}-${v}`).join(' ')) 308 | copyRange = rangeMap 309 | highlight(rangeMap) 310 | const useHex = getConfiguration('unot.useHex') 311 | 312 | if (useHex) { 313 | copyAttr = copyAttr.replace(/\[#([a-z0-9]+)\]/i, 'hex-$1') 314 | copyClass = copyClass.replace(/\[#([a-z0-9]+)\]/i, 'hex-$1') 315 | } 316 | 317 | const copyIcon = '' 318 | md.appendMarkdown('To Unocss:\n') 319 | md.appendMarkdown(`\nattributify: ${copyIcon} ${copyAttr}\n`) 320 | md.appendMarkdown('\n') 321 | md.appendMarkdown(`\nclass: ${copyIcon} ${copyClass}\n`) 322 | 323 | return createHover(md) 324 | } 325 | 326 | let hasUnoConfig: string | undefined 327 | const currentFolder = (vscode.workspace.workspaceFolders as any)?.[0] 328 | 329 | const switchMagic = getConfiguration('unot').get('switchMagic') 330 | 331 | let isOpen = switchMagic 332 | const statusBarItem = createBottomBar({ 333 | text: `uno-magic ${isOpen ? '✅' : '❌'}`, 334 | command: { 335 | title: 'uno-magic', 336 | command: 'unotmagic.changeStatus', 337 | }, 338 | position: 'left', 339 | offset: 500, 340 | }) 341 | 342 | const bottomBar = createBottomBar({ 343 | text: 'to-rem ❌', 344 | command: { 345 | title: 'unot-toRem', 346 | command: 'unotToRem.changeStatus', 347 | }, 348 | position: 'left', 349 | offset: 500, 350 | }) 351 | 352 | context.subscriptions.push(registerCommand('unotToRem.changeStatus', () => { 353 | toRemFlag = !toRemFlag 354 | bottomBar.text = `to-rem ${toRemFlag ? '✅' : '❌'}` 355 | })) 356 | 357 | bottomBar.show() 358 | 359 | if (currentFolder) 360 | await updateUnoStatus(getCurrentFileUrl()) 361 | if (presets.length) 362 | rules.unshift(...presets) 363 | 364 | registerCommand('unotmagic.changeStatus', () => { 365 | isOpen = !isOpen 366 | setConfiguration('unot.switchMagic', isOpen) 367 | statusBarItem.text = `uno-magic ${isOpen ? '✅' : '❌'}` 368 | }) 369 | 370 | context.subscriptions.push(addEventListener('text-save', (document: vscode.TextDocument) => { 371 | const activeTextEditor = getActiveTextEditor() 372 | if (!isOpen || !hasUnoConfig || !activeTextEditor) 373 | return 374 | // 对文档保存后的内容进行处理 375 | const text = document.getText() 376 | const { classAttr, attrs, styleChangeList } = parserAst(text) || {} 377 | const changeList: ChangeList[] = [] 378 | 379 | if (classAttr?.length) 380 | changeList.push(...transformClassAttr(classAttr as any)) 381 | if (attrs?.length) 382 | changeList.push(...transformAttrs(attrs)) 383 | if (styleChangeList?.length) { 384 | changeList.push(...styleChangeList.map((item: any) => { 385 | const start = getPosition(item.start) 386 | start.line = start.line + 1 387 | const end = getPosition(item.end) 388 | end.line = end.line + 1 389 | end.column = end.column + 1 390 | 391 | return { 392 | start, 393 | end, 394 | content: item.content, 395 | } 396 | })) 397 | } 398 | 399 | if (changeList.length) { 400 | updateText((edit) => { 401 | changeList.forEach((change: any) => { 402 | edit.replace(createRange([change.start.line - 1, change.start.column], [change.end.line - 1, change.end.column - 1]), change.content) 403 | }) 404 | }) 405 | nextTick(() => { 406 | // 文件已更新,调用保存 407 | saveFile() 408 | }) 409 | } 410 | })) 411 | 412 | context.subscriptions.push(addEventListener('activeText-change', async (editor) => { 413 | if (!editor || editor.document.languageId === 'Log') 414 | return 415 | 416 | const url = getCurrentFileUrl() 417 | if (!url) 418 | return 419 | await updateUnoStatus(url) 420 | if (!hasUnoConfig) 421 | statusBarItem.hide() 422 | else 423 | statusBarItem.show() 424 | })) 425 | 426 | if (!hasUnoConfig) { 427 | context.subscriptions.push(addEventListener('file-create', () => { 428 | updateUnoStatus() 429 | })) 430 | } 431 | 432 | function updateUnoStatus(cwd = currentFolder.uri.fsPath.replace(/\\/g, '/')) { 433 | const activeTextEditorUri = getCurrentFileUrl() 434 | if (!activeTextEditorUri) 435 | return 436 | if (activeTextEditorUri && !prefix.includes(activeTextEditorUri.split('.').slice(-1)[0])) { 437 | hasUnoConfig = undefined 438 | return 439 | } 440 | return findUp(['package.json'], { cwd }) 441 | .then((filepath?: string) => { 442 | if (!filepath) 443 | return 444 | for (const _unoSuffix of ['uno.config.js', 'uno.config.ts', 'unocss.config.js', 'unocss.config.ts']) { 445 | const configPath = path.resolve(filepath, '..', _unoSuffix) 446 | if (existsSync(configPath)) { 447 | hasUnoConfig = configPath 448 | statusBarItem.show() 449 | return 450 | } 451 | } 452 | hasUnoConfig = undefined 453 | statusBarItem.hide() 454 | }) 455 | } 456 | } 457 | 458 | export function deactivate() { 459 | cacheMap.clear() 460 | } 461 | -------------------------------------------------------------------------------- /src/openDocumentation.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { CreateWebview } from '@vscode-use/createwebview' 3 | import { registerCommand } from '@vscode-use/utils' 4 | import * as vscode from 'vscode' 5 | 6 | export function openDocumentation(context: ExtensionContext) { 7 | // webview 文档 8 | context.subscriptions.push(registerCommand('UnoT.openDocumentation', () => { 9 | const title = 'unocss documentation' 10 | const provider = new CreateWebview(context, { 11 | title, 12 | scripts: ['main.js'], 13 | viewColumn: vscode.ViewColumn.Beside, 14 | }) 15 | const url = 'https://unocss-docs.netlify.app/' 16 | provider.create(` 17 | 18 | 19 | 20 | 21 | 22 | ${title} 23 | 29 | 30 | 31 | 32 | 33 | 34 | `, ({ data, type }) => { 35 | // callback 获取js层的postMessage数据 36 | if (type === 'copy') { 37 | vscode.env.clipboard.writeText(data).then(() => { 38 | vscode.window.showInformationMessage('复制成功! ✅') 39 | }) 40 | } 41 | }) 42 | })) 43 | } 44 | -------------------------------------------------------------------------------- /src/openPlayground.ts: -------------------------------------------------------------------------------- 1 | import type { ExtensionContext } from 'vscode' 2 | import { CreateWebview } from '@vscode-use/createwebview' 3 | import { registerCommand } from '@vscode-use/utils' 4 | import * as vscode from 'vscode' 5 | 6 | export function openPlayground(context: ExtensionContext) { 7 | // webview 文档 8 | context.subscriptions.push(registerCommand('UnoT.openPlayground', () => { 9 | const title = 'unocss documentation' 10 | const provider = new CreateWebview(context, { 11 | title, 12 | scripts: ['main.js'], 13 | viewColumn: vscode.ViewColumn.Beside, 14 | }) 15 | const url = 'https://to-unocss.netlify.app/' 16 | provider.create(` 17 | 18 | 19 | 20 | 21 | 22 | ${title} 23 | 29 | 30 | 31 | 32 | 33 | 34 | `, ({ data, type }) => { 35 | // callback 获取js层的postMessage数据 36 | if (type === 'copy') { 37 | vscode.env.clipboard.writeText(data).then(() => { 38 | vscode.window.showInformationMessage('复制成功! ✅') 39 | }) 40 | } 41 | }) 42 | })) 43 | } 44 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import type { SFCTemplateBlock } from '@vue/compiler-sfc' 2 | import { parse as tsParser } from '@typescript-eslint/typescript-estree' 3 | import { getCurrentFileUrl, getOffsetFromPosition } from '@vscode-use/utils' 4 | import { parse } from '@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js' 5 | 6 | import * as vscode from 'vscode' 7 | 8 | const { parse: svelteParser } = require('svelte/compiler') 9 | 10 | // 引入vue-parser只在template中才处理一些逻辑 11 | let isInTemplate = false 12 | let isJsx = false 13 | export function parser(code: string, position: vscode.Position & { active: string }) { 14 | isJsx = false 15 | const entry = getCurrentFileUrl() 16 | if (!entry) 17 | return 18 | const suffix = entry.slice(entry.lastIndexOf('.') + 1) 19 | if (!suffix) 20 | return 21 | isInTemplate = false 22 | if (suffix === 'vue') { 23 | const result = transformVue(code, position) 24 | if (!result.refs) 25 | return result 26 | const refsMap = findRefs(result.template) 27 | return Object.assign(result, { refsMap }) 28 | } 29 | if (/jsx?|tsx?/.test(suffix)) 30 | return Object.assign(parserJSX(code, position), { isJsx }) 31 | 32 | if (suffix === 'svelte') 33 | return Object.assign(parserSvelte(code, position), { isJsx }) 34 | 35 | return true 36 | } 37 | 38 | export function transformVue(code: string, position: vscode.Position) { 39 | const { 40 | descriptor: { template, script, scriptSetup }, 41 | errors, 42 | } = parse(code) 43 | if (script?.lang === 'tsx') 44 | return Object.assign(parserJSX(code, position), { isJsx }) 45 | 46 | if (errors.length || !template) 47 | return 48 | const _script = script || scriptSetup 49 | isJsx = _script?.lang === 'tsx' 50 | if (_script && isInPosition(_script.loc, position)) { 51 | const content = _script.content! 52 | const refs: string[] = [] 53 | for (const match of content.matchAll(/(const|let|var)\s+([\w$]+)\s*=\s*ref[^()]*\(/g)) { 54 | if (match) 55 | refs.push(match[2]) 56 | } 57 | return { 58 | type: 'script', 59 | refs, 60 | template, 61 | isJsx, 62 | } 63 | } 64 | if (!isInPosition(template.loc, position)) 65 | return 66 | // 在template中 67 | const { ast } = template 68 | const r = dfs(ast!.children, template, position) 69 | return r || Object.assign(r, { isJsx }) 70 | } 71 | 72 | function dfs(children: any, parent: any, position: vscode.Position) { 73 | for (const child of children) { 74 | const { loc, tag, props, children } = child 75 | if (!isInPosition(loc, position)) 76 | continue 77 | if (props && props.length) { 78 | for (const prop of props) { 79 | if (isInPosition(prop.loc, position)) { 80 | if (!isInAttribute(child, position)) 81 | return false 82 | if ((prop.name === 'bind' || prop.name === 'on') && prop.arg) { 83 | return { 84 | tag, 85 | loc, 86 | propName: prop.arg.content, 87 | props, 88 | type: 'props', 89 | isInTemplate: true, 90 | isValue: !!prop?.value?.content, 91 | parent: { 92 | tag: parent.tag ? parent.tag : 'template', 93 | props: parent.props || [], 94 | }, 95 | isEvent: prop.name === 'on', 96 | } 97 | } 98 | else { 99 | return { 100 | tag, 101 | loc, 102 | propName: prop.name, 103 | props, 104 | type: 'props', 105 | isInTemplate: true, 106 | isValue: !!prop?.value?.content, 107 | parent: { 108 | tag: parent.tag ? parent.tag : 'template', 109 | props: parent.props || [], 110 | }, 111 | } 112 | } 113 | } 114 | } 115 | } 116 | if (children && children.length) { 117 | const result = dfs(children, child, position) as any 118 | if (result) 119 | return result 120 | } 121 | if (child.tag) { 122 | if (!isInAttribute(child, position)) 123 | return false 124 | return { 125 | type: 'props', 126 | loc, 127 | tag: child.tag, 128 | props, 129 | isInTemplate: true, 130 | parent: { 131 | tag: parent.tag ? parent.tag : 'template', 132 | props: parent.props || [], 133 | }, 134 | } 135 | } 136 | if (child.type === 2) { 137 | return { 138 | type: 'text', 139 | loc, 140 | isInTemplate: true, 141 | props, 142 | parent: { 143 | tag: parent.tag ? parent.tag : 'template', 144 | props: parent.props || [], 145 | }, 146 | } 147 | } 148 | return 149 | } 150 | } 151 | 152 | function isInPosition(loc: any, position: vscode.Position) { 153 | const { start, end } = loc 154 | const { line: startLine, column: startcharacter } = start 155 | const { line: endLine, column: endcharacter } = end 156 | const { line, character } = position 157 | if (line + 1 === startLine && character < startcharacter) 158 | return 159 | if (line + 1 === endLine && character > endcharacter - 1) 160 | return 161 | if (line + 1 < startLine) 162 | return 163 | if (line + 1 > endLine) 164 | return 165 | return true 166 | } 167 | 168 | export function parserJSX(code: string, position: vscode.Position) { 169 | const ast = tsParser(code, { jsx: true, loc: true }) 170 | const children = ast.body 171 | const result = jsxDfs(children, null, position) 172 | const map = findJsxRefs(children) 173 | if (result) 174 | return Object.assign(result, map) 175 | 176 | return { 177 | type: 'script', 178 | ...map, 179 | } 180 | } 181 | 182 | function jsxDfs(children: any, parent: any, position: vscode.Position) { 183 | for (const child of children) { 184 | let { loc, type, openingElement, body: children, argument, declarations, init } = child 185 | if (!loc) 186 | loc = convertPositionToLoc(child) 187 | 188 | if (!isInPosition(loc, position)) 189 | continue 190 | if (!openingElement && child.attributes) { 191 | openingElement = { 192 | name: { 193 | name: child.name, 194 | }, 195 | attributes: child.attributes, 196 | } 197 | } 198 | if (openingElement && openingElement.attributes.length) { 199 | for (const prop of openingElement.attributes) { 200 | if (!prop.loc) 201 | prop.loc = convertPositionToLoc(prop) 202 | if (isInPosition(prop.loc, position)) { 203 | return { 204 | tag: openingElement.name.type === 'JSXMemberExpression' 205 | ? `${openingElement.name.object.name}.${openingElement.name.property.name}` 206 | : openingElement.name.name, 207 | propName: typeof prop.name === 'string' ? prop.type === 'EventHandler' ? 'on' : prop.name : prop.name.name, 208 | props: openingElement.attributes, 209 | propType: prop.type, 210 | type: 'props', 211 | isInTemplate, 212 | loc, 213 | isValue: prop.value 214 | ? Array.isArray(prop.value) 215 | ? !!prop.value[0]?.raw 216 | : prop.value.type === 'JSXExpressionContainer' 217 | ? !!prop.value?.expression 218 | : !!prop?.value?.value 219 | : false, 220 | parent, 221 | isEvent: prop.type === 'EventHandler' || (prop.type === 'JSXAttribute' && prop.name.name.startsWith('on')), 222 | } 223 | } 224 | } 225 | } 226 | 227 | if (type === 'JSXElement' || type === 'Element' || (type === 'ReturnStatement' && (argument.type === 'JSXElement' || argument.type === 'JSXFragment'))) 228 | isInTemplate = true 229 | 230 | if (child.children) { children = child.children } 231 | else if (type === 'ExportNamedDeclaration') { children = child.declaration } 232 | else if (type === 'ObjectExpression') { children = child.properties } 233 | else if (type === 'Property' && child.value.type === 'FunctionExpression') { children = child.value.body.body } 234 | else if (type === 'ExportDefaultDeclaration') { 235 | if (child.declaration.type === 'FunctionDeclaration') 236 | children = child.declaration.body.body 237 | else 238 | children = child.declaration.arguments 239 | } 240 | else if (type === 'JSXExpressionContainer') { 241 | if (child.expression.type === 'CallExpression') { children = child.expression.arguments } 242 | else if (child.expression.type === 'ConditionalExpression') { 243 | children = [ 244 | child.expression.alternate, 245 | child.expression.consequent, 246 | ].filter(Boolean) 247 | } 248 | else if (child.expression.type === 'LogicalExpression') { 249 | children = [ 250 | child.expression.left, 251 | child.expression.right, 252 | ] 253 | } 254 | else { children = child.expression } 255 | } 256 | else if (type === 'TemplateLiteral') { 257 | children = child.expressions 258 | } 259 | else if (type === 'ConditionalExpression') { 260 | children = [ 261 | child.alternate, 262 | child.consequent, 263 | ].filter(Boolean) 264 | } 265 | else if (type === 'ArrowFunctionExpression') { 266 | children = child.body 267 | } 268 | else if (type === 'VariableDeclaration') { children = declarations } 269 | else if (type === 'VariableDeclarator') { children = init } 270 | else if (type === 'ReturnStatement') { children = argument } 271 | else if (type === 'JSXElement') { children = child.children } 272 | else if (type === 'ExportNamedDeclaration') { children = child.declaration.body } 273 | else if (type === 'CallExpression') { 274 | children = child.arguments 275 | } 276 | if (children && !Array.isArray(children)) 277 | children = [children] 278 | 279 | if (children && children.length) { 280 | const p = child.type === 'JSXElement' ? { name: openingElement.name.name, props: openingElement.attributes } : null 281 | const result = jsxDfs(children, p, position) as any 282 | if (result) 283 | return result 284 | } 285 | 286 | if (type === 'JSXElement' || type === 'Element' || type === 'InlineComponent') { 287 | const target = openingElement.attributes.find((item: any) => isInPosition(item.loc, position) || item.value === null) 288 | if (!openingElement) { 289 | openingElement = { 290 | name: { 291 | name: child.name, 292 | }, 293 | attributes: child.attributes, 294 | } 295 | } 296 | if (target) { 297 | return { 298 | type: 'props', 299 | loc, 300 | tag: openingElement.name.type === 'JSXMemberExpression' 301 | ? `${openingElement.name.object.name}.${openingElement.name.property.name}` 302 | : openingElement.name.name, 303 | props: openingElement.attributes, 304 | propName: target.value 305 | ? typeof target.name === 'string' 306 | ? target.type === 'EventHandler' 307 | ? 'on' 308 | : target.name 309 | : target.name.name 310 | : '', 311 | propType: target.type, 312 | isInTemplate, 313 | parent, 314 | } 315 | } 316 | return { 317 | type: 'props', 318 | loc, 319 | tag: openingElement.name.type === 'JSXMemberExpression' 320 | ? `${openingElement.name.object.name}.${openingElement.name.property.name}` 321 | : openingElement.name.name, 322 | props: openingElement.attributes, 323 | isInTemplate, 324 | parent, 325 | } 326 | } 327 | 328 | if (type === 'JSXText' || type === 'Text') { 329 | return { 330 | isInTemplate, 331 | loc, 332 | type: 'text', 333 | props: openingElement?.attributes, 334 | parent, 335 | } 336 | } 337 | return 338 | } 339 | } 340 | 341 | function findJsxRefs(children: any, map: any = {}, refs: any = []) { 342 | for (const child of children) { 343 | let { type, openingElement, body: children, argument, declarations, init, id, expression } = child 344 | if (type === 'VariableDeclaration') { 345 | children = declarations 346 | } 347 | else if (type === 'VariableDeclarator') { 348 | children = init 349 | if (init.callee && init.callee.name === 'useRef') { 350 | refs.push(id.name) 351 | continue 352 | } 353 | } 354 | else if (type === 'ExpressionStatement') { 355 | children = expression.arguments 356 | } 357 | else if (type === 'ReturnStatement') { 358 | children = argument 359 | } 360 | else if (type === 'JSXElement') { 361 | children = child.children 362 | } 363 | else if (!children) { 364 | continue 365 | } 366 | if (children && !Array.isArray(children)) 367 | children = [children] 368 | if (openingElement && openingElement.attributes.length) { 369 | for (const prop of openingElement.attributes) { 370 | if (prop.name.name === 'ref') { 371 | const value = prop.value?.expression?.name || prop.value.value 372 | map[value] = transformTagName(openingElement.name.name) 373 | } 374 | } 375 | } 376 | 377 | if (children && children.length) 378 | findJsxRefs(children, map, refs) 379 | } 380 | return { 381 | refsMap: map, 382 | refs, 383 | } 384 | } 385 | 386 | function findRefs(template: SFCTemplateBlock) { 387 | const { ast } = template 388 | return findRef(ast.children, {}) 389 | } 390 | function findRef(children: any, map: any) { 391 | for (const child of children) { 392 | const { tag, props, children } = child 393 | if (props && props.length) { 394 | for (const prop of props) { 395 | const { name, value } = prop 396 | if (!value) 397 | continue 398 | const { content } = value 399 | if (name !== 'ref' || !content) 400 | continue 401 | const tagName = transformTagName(tag) 402 | map[content] = tagName 403 | } 404 | } 405 | if (children && children.length) 406 | findRef(children, map) as any 407 | } 408 | return map 409 | } 410 | 411 | export function parserSvelte(code: string, position: vscode.Position) { 412 | const { html } = svelteParser(code) 413 | const result = jsxDfs([html], null, position) 414 | const map = { 415 | refsMap: {}, 416 | refs: [], 417 | } 418 | 419 | if (result) 420 | return Object.assign(result, map) 421 | 422 | return { 423 | type: 'script', 424 | ...map, 425 | } 426 | } 427 | 428 | export function transformTagName(name: string) { 429 | return name[0].toUpperCase() + name.replace(/(-\w)/g, (match: string) => match[1].toUpperCase()).slice(1) 430 | } 431 | 432 | export function isInAttribute(child: any, position: any) { 433 | const len = child.props.length 434 | let end = null 435 | const start = { 436 | column: child.loc.start.column + child.tag.length + 1, 437 | line: child.loc.start.line, 438 | offset: child.loc.start.offset + child.tag.length + 1, 439 | } 440 | if (!len) { 441 | const childNode = child.children?.[0] 442 | if (childNode) { 443 | end = { 444 | line: childNode.loc.start.line, 445 | column: childNode.loc.start.column - 1, 446 | offset: childNode.loc.start.offset - 1, 447 | } 448 | } 449 | else { 450 | if (child.isSelfClosing) { 451 | end = { 452 | line: child.loc.end.line, 453 | column: child.loc.end.column - 2, 454 | offset: child.loc.end.offset - 2, 455 | } 456 | } 457 | else { 458 | const startOffset = start.offset 459 | const match = child.loc.source.slice(child.tag.length + 1).match('>')! 460 | const endOffset = startOffset + match.index 461 | const offset = getOffsetFromPosition(position)! 462 | return (startOffset < offset) && (offset <= endOffset) 463 | } 464 | } 465 | } 466 | else { 467 | const offsetX = child.props[len - 1].loc.end.offset - child.loc.start.offset 468 | const x = child.loc.source.slice(offsetX).match('>').index! 469 | end = { 470 | column: child.props[len - 1].loc.end.column + 1 + x, 471 | line: child.props[len - 1].loc.end.line, 472 | offset: child.props[len - 1].loc.end.offset + 1 + x, 473 | } 474 | } 475 | 476 | const offset = getOffsetFromPosition(position)! 477 | const startOffset = start.offset 478 | const endOffset = end.offset 479 | return (startOffset < offset) && (offset < endOffset) 480 | } 481 | 482 | export function convertPositionToLoc(data: any) { 483 | const { start, end } = data 484 | const document = vscode.window.activeTextEditor!.document 485 | return { 486 | start: convertOffsetToLineColumn(document, start), 487 | end: convertOffsetToLineColumn(document, end), 488 | } 489 | } 490 | 491 | function convertOffsetToPosition(document: vscode.TextDocument, offset: number) { 492 | return document.positionAt(offset) 493 | } 494 | 495 | function convertOffsetToLineColumn(document: vscode.TextDocument, offset: number) { 496 | const position = convertOffsetToPosition(document, offset) 497 | const lineText = document.lineAt(position.line).text 498 | const line = position.line + 1 499 | const column = position.character + 1 500 | const lineOffset = document.offsetAt(position) 501 | 502 | return { line, column, lineText, lineOffset } 503 | } 504 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | import { getConfiguration } from '@vscode-use/utils' 2 | import { transfromCode } from 'transform-to-unocss' 3 | import { getCssType, getMultipedUnocssText } from './utils' 4 | 5 | export class CssToUnocssProcess { 6 | /** 7 | * transform multiple style to unocss 8 | * 9 | * @param {string} text origin text 10 | * @return {string} transformed text 11 | */ 12 | convert(text: string) { 13 | return getMultipedUnocssText(text) 14 | } 15 | 16 | /** 17 | * transform all page to unocss 18 | * 19 | * @param {string} code origin text 20 | * @return {string} transformed text 21 | */ 22 | async convertAll(code: string, fileName: string): Promise { 23 | if (!code) 24 | return '' 25 | const type = getCssType(fileName) as any 26 | const isJsx = getConfiguration('unot.classMode') 27 | return transfromCode(code, { filepath: fileName, type, isJsx }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/transform.ts: -------------------------------------------------------------------------------- 1 | import type { Attr, ChangeList } from './type' 2 | 3 | import { isCalc, isHex, isRgb } from 'transform-to-unocss-core' 4 | 5 | let variantGroup = true 6 | let strictVariable = true 7 | let strictHyphen = false 8 | try { 9 | ({ variantGroup, strictVariable, strictHyphen } = require('@vscode-use/utils').getConfiguration('unot')) 10 | } 11 | catch (error) { 12 | } 13 | const fontMap: any = { 14 | 100: 'thin', 15 | 200: 'extralight', 16 | 300: 'light', 17 | 400: 'normal', 18 | 500: 'medium', 19 | 600: 'semibold', 20 | 700: 'bold', 21 | 800: 'extrabold', 22 | 900: 'black', 23 | } 24 | 25 | const customMap: any = { 26 | 'b': 'border', 27 | 'bb': 'border-b', 28 | 'border-rd': 'rounded', 29 | 'lh': 'leading', 30 | } 31 | let classData: string[] = [] 32 | const COMMON_REG = strictHyphen 33 | ? /([!\s']|hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)([whmpb]|gapx|gapy|gap|mx|my|mt|mr|mb|ml|px|py|pt|pr|pb|pl|bt|br|bb|bl|lh|text|top|right|bottom|left|border-rd|border|max-w|max-h|min-w|min-h|translate-x|translate-y|duration|delay|scale-x|scale-y|scale|rotate|skew-x|skew-y|fill|stroke|invert|saturate|grayscale|contrast|brightness|blur|outline)-(-?\d+)(px|rem|em|%|vw|vh|!||$)/g 34 | : /([!\s']|hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)([whmpb]|gapx|gapy|gap|mx|my|mt|mr|mb|ml|px|py|pt|pr|pb|pl|bt|br|bb|bl|lh|text|top|right|bottom|left|border-rd|border|max-w|max-h|min-w|min-h|translate-x|translate-y|duration|delay|scale-x|scale-y|scale|rotate|skew-x|skew-y|fill|stroke|invert|saturate|grayscale|contrast|brightness|blur|outline)-?(-?\d+)(px|rem|em|%|vw|vh|!||$)/g 35 | const PSEUDO_CLASS = /(hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)\(([^)]+)\)(\s|'|$)/g 36 | export const rules: any = [ 37 | [/([\s!]?)([wh][wh]?)full([\s'!]|$)/g, (_: string, v0: string, v1: string, v2: string) => `${v1.split('').map(i => `${v0}${i}-full`).join(' ')}${v2}`], 38 | [/(['!\s]|hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)flex1([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}flex-1${v2}`], 39 | [/(['!\s]|hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)maxh([^\s']+)/, (_: string, v1: string, v2: string) => `${v1}max-h${v2}`], 40 | [/(['!\s]|hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)minh([^\s']+)/, (_: string, v1: string, v2: string) => `${v1}min-h${v2}`], 41 | [/(['!\s]|hover:|focus:|active:|disabled:|invalid:|checked:|required:|first:|last:|odd:|even:|after:|before:|placeholder:|file:|marker:|selection:|first-line:|first-letter:|backdrop:|md:|sm:|xl:|2xl:|lg:|dark:|ltr:|rtl:|group-hover:|group-focus:|group-active:)maxw([^\s']+)/, (_: string, v1: string, v2: string) => `${v1}max-w${v2}`], 42 | [/([\s'])minw([^\s']+)/, (_: string, v1: string, v2: string) => `${v1}min-w${v2}`], 43 | [/([\s!-])translatex([^\s']+)/, (_: string, v1 = '', v2: string) => `${v1}translate-x${v2}`], 44 | [/([\s!-])gapx([^\s']+)/, (_: string, v1 = '', v2: string) => `${v1}gap-x${v2}`], 45 | [/([\s!-])translatey([^\s']+)/, (_: string, v1: string, v2: string) => `${v1}translate-y${v2}`], 46 | [/([\s!-])gapy([^\s']+)/, (_: string, v1: string, v2: string) => `${v1}gap-y${v2}`], 47 | [COMMON_REG, (_: string, prefix: string, v: string, v1 = '', v2 = '') => { 48 | if (v in customMap) 49 | v = customMap[v] 50 | if ((v === 'border-b' || v === 'border') && v1 === '1') 51 | return `${prefix}${v}` 52 | if (v === 'text') { 53 | if (v2) 54 | return strictVariable ? `${prefix}${v}-[${v1}${v2}]` : `${prefix}${v}-${v1}${v2}` 55 | 56 | return `${prefix}${v}-${v1}` 57 | } 58 | return v2.trim() === '' 59 | ? strictVariable 60 | ? `${prefix}${v}-${v1}${v2}` 61 | : ['max-w', 'max-h', 'w', 'h', 'gap', 'gap-x', 'gap-y', 'mx', 'my', 'mt', 'mr', 'mb', 'ml', 'm', 'px', 'py', 'pt', 'pr', 'pb', 'pl', 'p'].includes(v) 62 | ? `${prefix}${v}${v1}${v2}` 63 | : `${prefix}${v}-${v1}${v2}` 64 | : strictVariable 65 | ? `${prefix}${v}-[${v1}${v2 === '!' ? '' : v2}]${v2 === '!' ? v2 : ''}` 66 | : `${prefix}${v}-${v1}${v2}` 67 | }], 68 | variantGroup 69 | ? [PSEUDO_CLASS, (_: string, prefix: string, v: string, v1 = '') => v 70 | .replace('flex-center', 'flex justify-center items-center') 71 | .replace(/\s+/g, ' ') 72 | .split(' ') 73 | .map(item => `${prefix}${item}`) 74 | .join(' ') + v1] 75 | : undefined, 76 | strictHyphen 77 | ? [/([\s'])(bg|text|border)-(#[^\s']+)([\s'!]|$)/g, (_: string, v: string, v1: string, v2: string, v3: string) => `${v}${v1}-${strictVariable ? '[' : ''}${v2}${strictVariable ? ']' : ''}${v3}`] 78 | : [/([\s'])(bg|text|border)-?(#[^\s']+)([\s'!]|$)/g, (_: string, v: string, v1: string, v2: string, v3: string) => `${v}${v1}-${strictVariable ? '[' : ''}${v2}${strictVariable ? ']' : ''}${v3}`], 79 | [/([\s'])border-box([\s'!]|$)/, (_: string, v1 = '', v2: string) => `${v1}box-border${v2}`], 80 | [/([\s'])content-box([\s'!]|$)/, (_: string, v1 = '', v2: string) => `${v1}box-content${v2}`], 81 | strictHyphen 82 | ? [/([A-Za-z]+)-(\[)?\s*(rgba?\([^)]*\))\]?([\s'!]|$)/g, (_: string, v0: string, bracket: string, v: string, v1: string) => `${v0}-${bracket || strictVariable ? '[' : ''}${v.replace(/\s*\/\s*/g, ',').replace(/\s+/g, ',').replace(/,+/g, ',')}${strictVariable ? ']' : ''}${v1}`] 83 | : [/([A-Za-z]+)-?(\[)?\s*(rgba?\([^)]*\))\]?([\s'!]|$)/g, (_: string, v0: string, bracket: string, v: string, v1: string) => `${v0}-${bracket || strictVariable ? '[' : ''}${v.replace(/\s*\/\s*/g, ',').replace(/\s+/g, ',').replace(/,+/g, ',')}${strictVariable ? ']' : ''}${v1}`], 84 | strictHyphen 85 | ? [/([A-Za-z]+)-(\[)?\s*(hsla?\([^)]*\))\]?([\s'!]|$)/g, (_: string, v0: string, bracket: string, v: string, v1: string) => `${v0}-${bracket || strictVariable ? '[' : ''}${v.replace(/\s*\/\s*/g, ',').replace(/\s+/g, ',').replace(/,+/g, ',')}${strictVariable ? ']' : ''}${v1}`] 86 | : [/([A-Za-z]+)-?(\[)?\s*(hsla?\([^)]*\))\]?([\s'!]|$)/g, (_: string, v0: string, bracket: string, v: string, v1: string) => `${v0}-${bracket || strictVariable ? '[' : ''}${v.replace(/\s*\/\s*/g, ',').replace(/\s+/g, ',').replace(/,+/g, ',')}${strictVariable ? ']' : ''}${v1}`], 87 | strictHyphen 88 | ? [/([A-Za-z]+)-(\[?)\s*(calc\([^)]*\))\]?([\s'!]|$)/g, (_: string, v0: string, bracket: string, v: string, v1 = '') => `${v0}-${bracket || strictVariable ? '[' : ''}${v.replace(/\s*/g, '')}${strictVariable ? ']' : ''}${v1}`] 89 | : [/([A-Za-z]+)-?(\[?)\s*(calc\([^)]*\))\]?([\s'!]|$)/g, (_: string, v0: string, bracket: string, v: string, v1 = '') => `${v0}-${bracket || strictVariable ? '[' : ''}${v.replace(/\s*/g, '')}${strictVariable ? ']' : ''}${v1}`], 90 | strictHyphen 91 | ? [/([A-Z]+)-(#[^\s']+)([\s'!]|$)/gi, (_: string, v0: string, v1: string, v2: string) => `${v0}-${strictVariable ? '[' : ''}${v1}${strictVariable ? ']' : ''}${v2}`] 92 | : [/([A-Z]+)-?(#[^\s']+)([\s'!]|$)/gi, (_: string, v0: string, v1: string, v2: string) => `${v0}-${strictVariable ? '[' : ''}${v1}${strictVariable ? ']' : ''}${v2}`], 93 | strictHyphen 94 | ? [/([A-Za-z]+)-(\d+)(px|vw|vh|rem|em|%)([\s'!]|$)/g, (_: string, v0: string, v1: string, v2 = '', v3 = '') => strictVariable ? `${v0}-[${v1}${v2}]${v3}` : `${v0}-${v1}${v2}${v3}`] 95 | : [/([A-Za-z]+)-?(\d+)(px|vw|vh|rem|em|%)([\s'!]|$)/g, (_: string, v0: string, v1: string, v2 = '', v3 = '') => strictVariable ? `${v0}-[${v1}${v2}]${v3}` : `${v0}-${v1}${v2}${v3}`], 96 | [strictHyphen 97 | ? /([\s!])(decoration|divide|ring|accent|stroke|fill|bb|bt|bl|br|bg|text|border)-\[?([^\s'\]]+)\]?(\s|'|$)/g 98 | : /([\s!])(decoration|divide|ring|accent|stroke|fill|bb|bt|bl|br|bg|text|border)-?\[?([^\s'\]]+)\]?(\s|'|$)/g, (_: string, v: string, v1: string, v2: string, v3: string) => { 99 | if (v1 in customMap) { 100 | v1 = customMap[v1] 101 | if (!classData.some(c => /border-/.test(c))) 102 | v3 = ` border-transparent${v3}` 103 | } 104 | 105 | if (v1 === 'border' && (isHex(v2) || isRgb(v2))) { 106 | const hasBorder = !!classData.find(item => /border$|border-\d|border-[bltr]$|-\d/.test(item)) 107 | const hasBorderStyle = !!classData.find(item => ['border-solid', 'border-dashed', 'border-dotted', 'border-double'].includes(item)) 108 | return `${v}${v1}-[${v2}]${hasBorder ? '' : ' border'}${hasBorderStyle ? '' : ' border-solid'}${v3}` 109 | } 110 | if (isHex(v2) || isRgb(v2) || isCalc(v2)) 111 | return `${v}${v1}-[${v2}]${v3}` 112 | return _ 113 | }], 114 | [/([\s!])x-hidden([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}overflow-x-hidden${v2}`], 115 | [/([\s!])y-hidden([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}overflow-y-hidden${v2}`], 116 | [/([\s!])justify-center([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}justify-center${v2}`], 117 | [/([\s!])align-center([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}items-center${v2}`], 118 | [/([\s'])eclipse(\s|'|$)/, (_: string, v1: string, v2: string) => `${v1}whitespace-nowrap overflow-hidden text-ellipsis${v2}`], 119 | [/([\s'])font-?(100|200|300|400|500|600|700|800|900)([\s'!]|$)/, (_: string, prefix: string, v1: string, v2: string) => `${prefix}font-${fontMap[v1]}${v2}`], 120 | [/([\s'])pointer-none([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}pointer-events-none${v2}`], 121 | [/([\s'])pointer([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}cursor-pointer${v2}`], 122 | [/([\s'])flex-center(\s|'|$)/, (_: string, v1: string, v2: string) => `${v1}${classData.includes('flex') ? '' : 'flex '}justify-center items-center${v2}`], 123 | [/([\s'])col(\s|'|$)/, (_: string, v1: string, v2: string) => `${v1}${classData.includes('flex') ? '' : 'flex '}flex-col${v2}`], 124 | [/([\s'])position-center(\s|'|$)/, (_: string, v1: string, v2: string) => `${v1}left-0 right-0 top-0 bottom-0${v2}`], 125 | [/([\s'])dashed([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}border-dashed${v2}`], 126 | [/([\s'])dotted([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}border-dotted${v2}`], 127 | [/([\s'])double([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}border-double${v2}`], 128 | [/([\s'])contain([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}bg-contain${v2}`], 129 | [/([\s'])cover([\s'!]|$)/, (_: string, v1: string, v2: string) => `${v1}bg-cover${v2}`], 130 | [/([\s'])line(\d+)([\s'!]|$)/, (_: string, v1: string, v2: string, v3: string) => `${v1}line-clamp-${v2}${v3}`], 131 | [/([\s!])\(([^)]+)\)([\s'!]|$)/, (_: string, v1: string, v2: string, v3: string) => { 132 | if (v1 === '!') 133 | return v2.replace(/\s+/g, ' ').split(' ').map(item => `!${item}`).join(' ') + v3 134 | if (v3 === '!') 135 | return v2.replace(/\s+/g, ' ').split(' ').map(item => `!${item}`).join(' ') 136 | return _ 137 | }], 138 | ].filter(Boolean) 139 | 140 | export function transform(content: string) { 141 | return rules.reduce((result: string, cur: [string | RegExp, string]) => { 142 | const [reg, callback] = cur 143 | return result.replace(/class(Name)?="([^"]*)"/g, (_: string, name = '', value: string) => { 144 | let v = ` ${value}` 145 | // 替换掉rgba内容排除掉 146 | let count = 0 147 | let temp = `__unot_split__${count}` 148 | const map: any = {} 149 | v = v.replace(/rgba?\([^)]+\)/g, (v) => { 150 | temp = `__unot_split__${count++}` 151 | map[temp] = v 152 | return temp 153 | }) 154 | 155 | const matcher = v.match(/([\s'])(\w+)-\[(([-\s\w%()*+,/:][-\s\w%()*+/:]*,[-\s\w%()*+,/:]+)+)\](\s|'|$)/) 156 | if (matcher && matcher[3].includes(',')) { 157 | try { 158 | v = `${matcher[1]}${matcher[3].split(',').map((item) => { 159 | if (item.includes('rgb') && !/rgba?\([^)]+\)/.test(item)) 160 | throw new Error('match error') 161 | if (item.includes('calc') && !/calc\([^)]+\)/.test(item)) 162 | throw new Error('match error') 163 | 164 | if (item.includes(':')) { 165 | const items = item.split(':') 166 | return `${items.slice(0, -1).join(':')}:${matcher[2]}-${items.slice(-1)[0]}` 167 | } 168 | return `${matcher[2]}-${item}` 169 | }).join(' ')}` 170 | } 171 | catch (error) { 172 | return _ 173 | } 174 | } 175 | Object.keys(map).forEach((key) => { 176 | v = v.replace(key, map[key]) 177 | }) 178 | classData = value.split(' ').map(item => item.replace(/['[\]]/g, '')) 179 | 180 | const newClass = v 181 | .replace(/\s([^!\s'"]+)!/g, (_, v) => ` !${v}`) 182 | .replace(reg, callback) 183 | .slice(1) 184 | return `class${name}="${newClass}"` 185 | }) 186 | }, content) 187 | } 188 | 189 | export function transformClass(attr: string) { 190 | return rules.reduce((result: string, cur: [string | RegExp, string]) => { 191 | const [reg, callback] = cur 192 | let v = ` ${result}` 193 | // 替换掉rgba内容排除掉 194 | let count = 0 195 | let temp = `__unocss_magic_split__${count}` 196 | const map: any = {} 197 | v = v.replace(/rgba?\([^)]+\)/g, (v) => { 198 | temp = `__unocss_magic_split__${count++}` 199 | map[temp] = v 200 | return temp 201 | }) 202 | 203 | const matcher = v.match(/([\s'])(\w+)-\[(([-\s\w%()*+,/:][-\s\w%()*+/:]*,[-\s\w%()*+,/:]+)+)\](\s|'|$)/) 204 | if (matcher && matcher[3].includes(',')) { 205 | try { 206 | v = `${matcher[1]}${matcher[3].split(',').map((item) => { 207 | if (item.includes('rgb') && !/rgba?\([^)]+\)/.test(item)) 208 | throw new Error('match error') 209 | if (item.includes('calc') && !/calc\([^)]+\)/.test(item)) 210 | throw new Error('match error') 211 | 212 | if (item.includes(':')) { 213 | const items = item.split(':') 214 | return `${items.slice(0, -1).join(':')}:${matcher[2]}-${items.slice(-1)[0]}` 215 | } 216 | return `${matcher[2]}-${item}` 217 | }).join(' ')}` 218 | } 219 | catch (error) { 220 | return result 221 | } 222 | } 223 | Object.keys(map).forEach((key) => { 224 | v = v.replace(key, map[key]) 225 | }) 226 | classData = result.split(' ') 227 | const newClass = v.replace(reg, callback).slice(1) 228 | return newClass 229 | }, attr) 230 | } 231 | 232 | export function transformClassAttr(attrs: Attr[]) { 233 | const changeList: ChangeList[] = [] 234 | attrs.forEach((attr) => { 235 | const { content, start, end } = attr 236 | const newAttr = transformClass(content) 237 | if (content !== newAttr) { 238 | changeList.push({ 239 | content: newAttr, 240 | start, 241 | end, 242 | }) 243 | } 244 | }) 245 | return changeList 246 | } 247 | 248 | const attrRules = strictHyphen 249 | ? /-(\[?\s*(rgba?\([^)]*\))\]|\[?\s*(hsla?\([^)]*\))\]|\[?\s*(calc\([^)]*\))\])/g 250 | : /-?(\[?\s*(rgba?\([^)]*\))\]|\[?\s*(hsla?\([^)]*\))\]|\[?\s*(calc\([^)]*\))\])/g 251 | const attrsMap = ['w', 'h', 'gap', 'm', 'mx', 'my', 'mt', 'mr', 'mb', 'ml', 'p', 'px', 'py', 'pt', 'pr', 'pb', 'pl', 'b', 'bt', 'br', 'bb', 'bl', 'lh', 'text', 'top', 'right', 'bottom', 'left', 'border-rd', 'border', 'max-w', 'max-h', 'translate-x', 'translate-y', 'duration', 'delay', 'scale-x', 'scale-y', 'scale', 'rotate', 'skew-x', 'skew-y', 'fill', 'stroke', 'invert', 'saturate', 'grayscale', 'contrast', 'brightness', 'blur', 'outline'] 252 | 253 | export function transformAttrs(attrs: any[]) { 254 | const changeList: ChangeList[] = [] 255 | attrs.forEach((attr) => { 256 | const { content, start, end, attrName } = attr 257 | if (!attrsMap.includes(attrName)) 258 | return 259 | let newAttr = content 260 | for (const match of content.matchAll(attrRules)) { 261 | const index = match.index 262 | newAttr = `${newAttr.slice(0, index)}[${match[2].replace(/\s+/g, '')}]${newAttr.slice(index + match[0].length)}` 263 | } 264 | 265 | if (content !== newAttr) { 266 | changeList.push({ 267 | content: newAttr, 268 | start, 269 | end, 270 | }) 271 | } 272 | }) 273 | return changeList 274 | } 275 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | export interface Attr { 2 | content: string 3 | start: any 4 | end: any 5 | line: number 6 | charater: number 7 | } 8 | 9 | export interface ChangeList { 10 | content: string 11 | start: any 12 | end: any 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fsp from 'node:fs/promises' 2 | import { parse as tsParser } from '@typescript-eslint/typescript-estree' 3 | import { getActiveTextEditor } from '@vscode-use/utils' 4 | import { parse } from '@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js' 5 | import fg from 'fast-glob' 6 | import { toUnocss } from 'transform-to-unocss-core' 7 | import * as vscode from 'vscode' 8 | import { decorationType, toRemFlag } from '../' 9 | import { transformClass } from '../transform' 10 | 11 | export type CssType = 'less' | 'scss' | 'css' | 'stylus' 12 | export function getCssType(filename: string) { 13 | const data = filename.split('.') 14 | const ext = data.pop()! 15 | const result = ext === 'styl' ? 'stylus' : ext 16 | return result as CssType 17 | } 18 | 19 | function hyphenate(str: string) { 20 | return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() 21 | } 22 | export function getMultipedUnocssText(text: string) { 23 | const match = text.match(/style="([^"]+)"/) || text.match(/style=\{\{?([^}]+)\}?\}/) 24 | if (match) 25 | text = match[1] 26 | 27 | const selectedTexts = text.split(';').filter(i => i !== '"') 28 | let isChanged = false 29 | const selectedNewTexts = [] 30 | for (let i = 0; i < selectedTexts.length; i++) { 31 | let text = hyphenate(selectedTexts[i]) 32 | // 要将驼峰转换为短横线 33 | // 例如:fontSize => font-size 34 | if (/:\s*["'].*["']/.test(text)) { 35 | text = text.replace(/:\s*(["'])(.*)\1/g, (all, _, v) => `:${v}`) 36 | } 37 | const newText = toUnocss(text) ?? text 38 | if (!newText) 39 | continue 40 | if (!isChanged) 41 | isChanged = newText !== text 42 | selectedNewTexts.push(toRemFlag 43 | ? newText.replace(/-([0-9.]+)px/, (_: string, v: string) => `-${+v / 4}`) 44 | : newText) 45 | } 46 | // 没有存在能够转换的元素 47 | if (!isChanged) 48 | return 49 | 50 | const selectedUnocssText = selectedNewTexts.join(' ') 51 | return selectedUnocssText 52 | } 53 | export class LRUCache { 54 | private cache 55 | private maxSize 56 | constructor(maxSize: number) { 57 | this.cache = new Map() 58 | this.maxSize = maxSize 59 | } 60 | 61 | get(key: any) { 62 | // 获取缓存值,并将其从Map中删除再重新插入,保证其成为最新的元素 63 | const value = this.cache.get(key) 64 | if (value !== undefined) { 65 | this.cache.delete(key) 66 | this.cache.set(key, value) 67 | } 68 | return value 69 | } 70 | 71 | set(key: any, value: any) { 72 | // 如果缓存已满,先删除最旧的元素 73 | if (this.cache.size >= this.maxSize) { 74 | const oldestKey = this.cache.keys().next().value 75 | this.cache.delete(oldestKey) 76 | } 77 | // 插入新值 78 | this.cache.set(key, value) 79 | } 80 | 81 | has(key: any) { 82 | return this.cache.has(key) 83 | } 84 | 85 | clear() { 86 | return this.cache.clear() 87 | } 88 | } 89 | 90 | export async function hasFile(source: string | string[]) { 91 | const workspaceFolders = vscode.workspace.workspaceFolders 92 | if (!workspaceFolders) 93 | return [] 94 | const cwd = workspaceFolders[0].uri.fsPath 95 | const entries = await fg(source, { 96 | cwd, 97 | ignore: ['**/dist/**', '**/node_modules/**'], 98 | }) 99 | 100 | return await Promise.all(entries.map((relativepath) => { 101 | const absolutepath = `${cwd}/${relativepath}` 102 | return fsp.readFile(absolutepath, 'utf-8') 103 | })) 104 | } 105 | 106 | export const disposes: any = [] 107 | 108 | export function highlight(realRangeMap: vscode.Range[]) { 109 | const editor = getActiveTextEditor() 110 | if (!editor) 111 | return 112 | editor.edit(() => editor.setDecorations(decorationType, realRangeMap)) 113 | } 114 | 115 | export function resetDecorationType() { 116 | return getActiveTextEditor()?.setDecorations(decorationType, []) 117 | } 118 | 119 | export function parserAst(code: string) { 120 | const entry = getActiveTextEditor()?.document.uri.fsPath 121 | if (!entry) 122 | return 123 | const suffix = entry.slice(entry.lastIndexOf('.') + 1) 124 | if (!suffix) 125 | return 126 | if (suffix === 'vue') 127 | return transformVueAst(code) 128 | if (/jsx?|tsx?/.test(suffix)) 129 | return parserJSXAst(code) 130 | } 131 | 132 | export function transformVueAst(code: string) { 133 | const { 134 | descriptor: { template, script, scriptSetup, styles }, 135 | errors, 136 | } = parse(code) 137 | 138 | const styleChangeList: { content: string, start: number, end: number }[] = [] 139 | if (styles.length) { 140 | styles.forEach((style: any) => { 141 | for (const match of style.content.matchAll(/\s*(?:@|--at-)apply:([^;]*);/g)) { 142 | const content = match[1] 143 | const newAttr = transformClass(content) 144 | if (content.trim() !== newAttr.trim()) { 145 | const start = style.loc.start.offset + match.index + match[0].indexOf(content) 146 | const end = start + content.length 147 | styleChangeList.push({ 148 | content: newAttr, 149 | start, 150 | end, 151 | }) 152 | } 153 | } 154 | }) 155 | } 156 | 157 | if ((script || scriptSetup)?.lang === 'tsx' && (script || scriptSetup)?.content) 158 | return parserJSXAst((script || scriptSetup)!.content) 159 | if (errors.length || !template) 160 | return 161 | 162 | // 在template中 163 | const { ast } = template 164 | 165 | return Object.assign({}, jsxAstDfs(ast.children), { styleChangeList }) 166 | } 167 | function jsxAstDfs(children: any, result: { classAttr: any[], attrs: any[] } = { classAttr: [], attrs: [] }) { 168 | for (const child of children) { 169 | const { props, children } = child 170 | if (props && props.length) { 171 | for (const prop of props) { 172 | if (prop.name === 'class' && prop.value) { 173 | prop.value.loc.end.column = prop.value.loc.start.column + prop.value.loc.source.length - 1 174 | result.classAttr.push({ 175 | content: prop.value.content, 176 | line: prop.value.loc.start.line, 177 | charater: prop.value.loc.start.column, 178 | start: prop.value.loc.start, 179 | end: prop.value.loc.end, 180 | attrName: prop.name, 181 | }) 182 | } 183 | else if (prop.name === 'bind' && prop.arg && prop.arg.content === 'class') { 184 | const start = { 185 | column: prop.exp.loc.start.column - 1, 186 | line: prop.exp.loc.start.line, 187 | } 188 | result.classAttr.push({ 189 | content: prop.exp.content, 190 | line: prop.exp.loc.start.line, 191 | charater: prop.exp.loc.start.column, 192 | start, 193 | end: prop.exp.loc.end, 194 | attrName: prop.name, 195 | }) 196 | } 197 | else if (props.name === 'bind') { 198 | const start = { 199 | column: prop.exp.loc.start.column - 1, 200 | line: prop.exp.loc.start.line, 201 | } 202 | result.attrs.push({ 203 | content: prop.exp.content, 204 | line: prop.exp.loc.start.line, 205 | charater: prop.exp.loc.start.column, 206 | start, 207 | end: prop.exp.loc.end, 208 | attrName: prop.name, 209 | }) 210 | } 211 | else if (prop.name && prop.value) { 212 | prop.value.loc.end.column = prop.value.loc.start.column + prop.value.loc.source.length - 1 213 | result.attrs.push({ 214 | content: prop.value.content, 215 | line: prop.value.loc.start.line, 216 | charater: prop.value.loc.start.column, 217 | start: prop.value.loc.start, 218 | end: prop.value.loc.end, 219 | attrName: prop.name, 220 | }) 221 | } 222 | } 223 | } 224 | if (children && children.length) 225 | jsxAstDfs(children, result) as any 226 | } 227 | return result 228 | } 229 | 230 | export function parserJSXAst(code: string) { 231 | const ast = tsParser(code, { jsx: true, loc: true }) 232 | return jsxDfsAst(ast.body) 233 | } 234 | 235 | function jsxDfsAst(children: any, result: { classAttr: any[], attrs: any[] } = { classAttr: [], attrs: [] }) { 236 | for (const child of children) { 237 | let { type, openingElement, body: children, argument, declaration, declarations, init } = child 238 | if (openingElement && openingElement.attributes.length) { 239 | for (const prop of openingElement.attributes) { 240 | if (prop.value && prop.value.type === 'Literal') { 241 | if (prop.name.name === 'className' || prop.name.name === 'class') { 242 | prop.value.loc.start.column++ 243 | result.classAttr.push({ 244 | content: prop.value.value, 245 | start: prop.value.loc.start, 246 | end: prop.value.loc.end, 247 | attrName: prop.name.name, 248 | }) 249 | } 250 | else if (prop.name.name) { 251 | prop.value.loc.start.column++ 252 | result.attrs.push({ 253 | content: prop.value.value, 254 | start: prop.value.loc.start, 255 | end: prop.value.loc.end, 256 | attrName: prop.name.name, 257 | }) 258 | } 259 | } 260 | } 261 | } 262 | 263 | if (type === 'VariableDeclaration') { children = declarations } 264 | else if (type === 'VariableDeclarator') { children = init } 265 | else if (type === 'ReturnStatement') { children = argument } 266 | else if (type === 'JSXElement') { children = child.children } 267 | else if (type === 'ExportDefaultDeclaration' || (type === 'ExportNamedDeclaration' && declaration.type === 'FunctionDeclaration')) { 268 | if (declaration.callee.name === 'defineComponent') { 269 | const properties = declaration.arguments[0]?.properties 270 | const result = [] 271 | if (properties) { 272 | for (const p of properties) { 273 | if (p.key.name === 'setup' || p.key.name === 'render') 274 | result.push(p.value) 275 | } 276 | } 277 | children = result 278 | } 279 | else { children = declaration?.body?.body } 280 | } 281 | else if (type === 'ExportNamedDeclaration') { children = declaration.declarations } 282 | 283 | if (children && !Array.isArray(children)) 284 | children = [children] 285 | 286 | if (children && children.length) 287 | jsxDfsAst(children, result) as any 288 | } 289 | return result 290 | } 291 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { transform, transformClass } from '../src/transform' 3 | 4 | describe('should', () => { 5 | it('bg', () => { 6 | expect(transform('class="bg-rgba(0,0,0) text-[#fff]"')).toMatchInlineSnapshot('"class=\\"bg-[rgba(0,0,0)] text-[#fff]\\""') 7 | }) 8 | it('-translatex1px', () => { 9 | expect(transform('class="-translatex1px"')).toMatchInlineSnapshot('"class=\\"-translate-x-[1px]\\""') 10 | }) 11 | it('maxw100%', () => { 12 | expect(transform('class="maxw100%"')).toMatchInlineSnapshot('"class=\\"max-w-[100%]\\""') 13 | }) 14 | it('exported', () => { 15 | expect(transform(`class=" 16 | xxmax-w 17 | max-w-1 18 | xx-col flex-col-x"`)).toMatchInlineSnapshot(` 19 | "class=\\" 20 | xxmax-w 21 | max-w-1 22 | xx-col flex-col-x\\"" 23 | `) 24 | }) 25 | it('hsl', () => { 26 | expect(transform('class="bg-hsl(150 , 30% , 60% , 0.8)"')).toMatchInlineSnapshot('"class=\\"bg-[hsl(150,30%,60%,0.8)]\\""') 27 | }) 28 | it('rgb', () => { 29 | expect(transform('class="bg-rgba(150 30% 60% / 0.8)"')).toMatchInlineSnapshot('"class=\\"bg-[rgba(150,30%,60%,0.8)]\\""') 30 | }) 31 | 32 | it('calc', () => { 33 | expect(transform('class=" w-calc(100% - 20px) "')).toMatchInlineSnapshot('"class=\\" w-[calc(100%-20px)] \\""') 34 | }) 35 | it('text-[]', () => { 36 | expect( 37 | transform('class="text-[rgba(1,1,1,1),hover:pink,2xl,lg:hover:3xl]"'), 38 | ).toMatchInlineSnapshot('"class=\\"text-[rgba(1,1,1,1)] hover:text-pink text-2xl lg:hover:text-3xl\\""') 39 | }) 40 | it('translatex50rem', () => { 41 | expect( 42 | transform('class="-translatex50rem!"'), 43 | ).toMatchInlineSnapshot('"class=\\"!-translate-x-[50rem]\\""') 44 | }) 45 | it('text', () => { 46 | expect( 47 | transform('class="text#fff!"'), 48 | ).toMatchInlineSnapshot('"class=\\"!text-[#fff]\\""') 49 | }) 50 | it('textrgba', () => { 51 | expect( 52 | transform('class="textrgba(1,2,3,.1)!"'), 53 | ).toMatchInlineSnapshot('"class=\\"!text-[rgba(1,2,3,.1)]\\""') 54 | }) 55 | it(':class="[\'hcalc(100vh-104px)\']"', () => { 56 | expect( 57 | transform(':class="[\'hcalc(100vh-104px)\']"'), 58 | ).toMatchInlineSnapshot('":class=\\"[\'h-[calc(100vh-104px)]\']\\""') 59 | }) 60 | it('hcalc(100vh-104px)', () => { 61 | expect( 62 | transform('class="hcalc(100vh-104px)"'), 63 | ).toMatchInlineSnapshot('"class=\\"h-[calc(100vh-104px)]\\""') 64 | }) 65 | it('hover:()', () => { 66 | expect( 67 | transform(':class="hover:(text-white rounded)"'), 68 | ).toMatchInlineSnapshot('":class=\\"hover:text-white hover:rounded\\""') 69 | }) 70 | it('hover:(flex-center)', () => { 71 | expect( 72 | transform(':class="hover:(flex-center) w10"'), 73 | ).toMatchInlineSnapshot('":class=\\"hover:flex hover:justify-center hover:items-center w-10\\""') 74 | }) 75 | it('hover:(flex-center) w10', () => { 76 | expect( 77 | transform(':class="hover:(flex-center) w10"'), 78 | ).toMatchInlineSnapshot('":class=\\"hover:flex hover:justify-center hover:items-center w-10\\""') 79 | }) 80 | it('w10 !(flex-center w10) w20', () => { 81 | expect( 82 | transform(':class=" w10 !(flex-center w10) w20"'), 83 | ).toMatchInlineSnapshot('":class=\\" w-10 !flex-center !w-10 w-20\\""') 84 | }) 85 | it('top10', () => { 86 | expect( 87 | transform(':class=" top10 gapx-1"'), 88 | ).toMatchInlineSnapshot('":class=\\" top-10 gap-x-1\\""') 89 | expect( 90 | transform(':class=" [x?\'top10\': \'gapx1\']"'), 91 | ).toMatchInlineSnapshot('":class=\\" [x?\'top-10\': \'gapx-1\']\\""') 92 | }) 93 | it('w', () => { 94 | expect( 95 | transform(':class=" w15!"'), 96 | ).toMatchInlineSnapshot('":class=\\" !w-15\\""') 97 | 98 | expect( 99 | transform(':class=" border#fff"'), 100 | ).toMatchInlineSnapshot('":class=\\" border-[#fff] border border-solid\\""') 101 | }) 102 | it('whfull', () => { 103 | expect( 104 | transform(':class=" whfull!"'), 105 | ).toMatchInlineSnapshot('":class=\\" !w-full !h-full\\""') 106 | expect( 107 | transform(':class=" wfull"'), 108 | ).toMatchInlineSnapshot('":class=\\" w-full\\""') 109 | expect( 110 | transform(':class="hfull"'), 111 | ).toMatchInlineSnapshot('":class=\\"h-full\\""') 112 | }) 113 | it('bb$color', () => { 114 | expect( 115 | transform(':class=" bb#eee"'), 116 | ).toMatchInlineSnapshot('":class=\\" border-b-[#eee] border-transparent\\""') 117 | }) 118 | it('shadow', () => { 119 | expect( 120 | transform(':class=" shadow-[0px_5px_10px_1px_rgba(1,1,1,1)] bg-rgba(1,1,1,1) bg#eee"'), 121 | ).toMatchInlineSnapshot('":class=\\" shadow-[0px_5px_10px_1px_rgba(1,1,1,1)] bg-[rgba(1,1,1,1)] bg-[#eee]\\""') 122 | }) 123 | it('magic transform strictVariable', () => { 124 | expect(transform(':class=" w1"')).toMatchInlineSnapshot('":class=\\" w-1\\""') 125 | expect(transform(':class=" pt8"')).toMatchInlineSnapshot('":class=\\" pt-8\\""') 126 | expect(transform(':class=" bgrgba(1,1,1,1)"')).toMatchInlineSnapshot('":class=\\" bg-[rgba(1,1,1,1)]\\""') 127 | }) 128 | it('magic transformClass w!', () => { 129 | expect(transformClass('
')).toMatchInlineSnapshot('"
"') 130 | expect(transformClass('
')).toMatchInlineSnapshot('"
"') 131 | expect(transformClass('{ active: modelValue === (tab.name || index) }')).toMatchInlineSnapshot('"{ active: modelValue === (tab.name || index) }"') 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /test/transformUno.test.ts: -------------------------------------------------------------------------------- 1 | import { toUnocssClass, transformStyleToUnocss } from 'transform-to-unocss-core' 2 | import { describe, expect, it } from 'vitest' 3 | 4 | describe('should', () => { 5 | it('toUnocssClass', () => { 6 | expect(toUnocssClass(`width: 550px; 7 | height: 307px; 8 | background: #000000;`)[0]).toMatchInlineSnapshot('"w-550px h-307px bg-[#000000]"') 9 | }) 10 | it('transformStyleToUnocss', () => { 11 | expect(transformStyleToUnocss(`width: 550px; 12 | height: 307px; 13 | background: #000000;`)[0]).toMatchInlineSnapshot('"w-550px h-307px bg=\\"[#000000]\\""') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["esnext"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | export default defineConfig({ 4 | target: 'node14', 5 | format: ['cjs'], 6 | external: [ 7 | 'vscode', 8 | ], 9 | // minify: true, 10 | clean: true, 11 | platform: 'node', // 明确指定为 Node.js 平台 12 | }) 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | target: 'node14', 5 | format: ['cjs'], 6 | external: [ 7 | 'vscode', 8 | ], 9 | // minify: true, 10 | clean: true, 11 | platform: 'node', // 明确指定为 Node.js 平台 12 | }) 13 | --------------------------------------------------------------------------------