├── .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 |
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 | 
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 |
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 | 
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'">]/.test(lineText[pre]))) {
114 | prefixName = `${lineText[pre]}${prefixName}`
115 | pre--
116 | }
117 | }
118 |
119 | let transferred = ''
120 | let noTransferred = []
121 | if (prefixName)
122 | [transferred, noTransferred] = toUnocssClass(copyText)
123 | else
124 | [transferred, noTransferred] = transformStyleToUnocss(copyText)
125 |
126 | if (noTransferred.length) {
127 | message.error(`${isZh ? '⚠️ 有一些属性unocss暂时还不支持转换,请自行处理:' : '⚠️ Some attributes unocss do not support conversion for the time being, please deal with them by yourself: '}${noTransferred.join('; ')}`)
128 | return
129 | }
130 |
131 | updateText((builder) => {
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 |
--------------------------------------------------------------------------------