chore: init frontend with knowledge module
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import type { Plugin } from "vite";
|
||||
import { SAFE_CHAR_MAP_LOCALE } from "./config";
|
||||
import { createCtx } from "../ctx";
|
||||
import { readFile, rootDir } from "../utils";
|
||||
|
||||
// 获取 tailwind.config.ts 中的颜色
|
||||
function getTailwindColor() {
|
||||
const config = readFile(rootDir("tailwind.config.ts"));
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从配置文件中动态提取主色和表面色
|
||||
const colorResult: Record<string, string> = {};
|
||||
|
||||
// 提取 getPrimary 调用中的颜色名称
|
||||
const primaryMatch = config.match(/getPrimary\(["']([^"']+)["']\)/);
|
||||
const primaryColorName = primaryMatch?.[1];
|
||||
|
||||
// 提取 getSurface 调用中的颜色名称
|
||||
const surfaceMatch = config.match(/getSurface\(["']([^"']+)["']\)/);
|
||||
const surfaceColorName = surfaceMatch?.[1];
|
||||
|
||||
if (primaryColorName) {
|
||||
// 提取 PRIMARY_COLOR_PALETTES 中对应的调色板
|
||||
const primaryPaletteMatch = config.match(
|
||||
new RegExp(
|
||||
`{\\s*name:\\s*["']${primaryColorName}["'],\\s*palette:\\s*({[^}]+})`,
|
||||
"s",
|
||||
),
|
||||
);
|
||||
|
||||
if (primaryPaletteMatch) {
|
||||
// 解析调色板对象
|
||||
const paletteStr = primaryPaletteMatch[1];
|
||||
const paletteEntries = paletteStr.match(/(\d+):\s*["']([^"']+)["']/g);
|
||||
|
||||
if (paletteEntries) {
|
||||
paletteEntries.forEach((entry: string) => {
|
||||
const match = entry.match(/(\d+):\s*["']([^"']+)["']/);
|
||||
if (match) {
|
||||
const [, key, value] = match;
|
||||
colorResult[`primary-${key}`] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (surfaceColorName) {
|
||||
// 提取 SURFACE_PALETTES 中对应的调色板
|
||||
const surfacePaletteMatch = config.match(
|
||||
new RegExp(
|
||||
`{\\s*name:\\s*["']${surfaceColorName}["'],\\s*palette:\\s*({[^}]+})`,
|
||||
"s",
|
||||
),
|
||||
);
|
||||
|
||||
if (surfacePaletteMatch) {
|
||||
// 解析调色板对象
|
||||
const paletteStr = surfacePaletteMatch[1];
|
||||
const paletteEntries = paletteStr.match(/(\d+):\s*["']([^"']+)["']/g);
|
||||
|
||||
if (paletteEntries) {
|
||||
paletteEntries.forEach((entry: string) => {
|
||||
const match = entry.match(/(\d+):\s*["']([^"']+)["']/);
|
||||
if (match) {
|
||||
const [, key, value] = match;
|
||||
// 0 对应 surface,其他对应 surface-*
|
||||
const colorKey = key === "0" ? "surface" : `surface-${key}`;
|
||||
colorResult[colorKey] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return colorResult;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取版本号
|
||||
function getVersion() {
|
||||
const pkg = readFile(rootDir("package.json"), true);
|
||||
return pkg?.version || "0.0.0";
|
||||
}
|
||||
|
||||
export function codePlugin(): Plugin[] {
|
||||
return [
|
||||
{
|
||||
name: "vite-cool-uniappx-code-pre",
|
||||
enforce: "pre",
|
||||
async transform(code, id) {
|
||||
if (id.includes("/cool/ctx/index.ts")) {
|
||||
const ctx = await createCtx();
|
||||
|
||||
// 主题配置
|
||||
const theme = readFile(rootDir("theme.json"), true);
|
||||
|
||||
// 主题配置
|
||||
ctx["theme"] = theme || {};
|
||||
|
||||
// 颜色值
|
||||
ctx["color"] = getTailwindColor();
|
||||
|
||||
if (!ctx.subPackages) {
|
||||
ctx.subPackages = [];
|
||||
}
|
||||
|
||||
if (!ctx.tabBar) {
|
||||
ctx.tabBar = {};
|
||||
}
|
||||
|
||||
if (!ctx.uniIdRouter) {
|
||||
ctx.uniIdRouter = {};
|
||||
}
|
||||
|
||||
// 安全字符映射
|
||||
ctx["SAFE_CHAR_MAP_LOCALE"] = [];
|
||||
for (const i in SAFE_CHAR_MAP_LOCALE) {
|
||||
ctx["SAFE_CHAR_MAP_LOCALE"].push([i, SAFE_CHAR_MAP_LOCALE[i]]);
|
||||
}
|
||||
|
||||
let ctxCode = JSON.stringify(ctx, null, 4);
|
||||
|
||||
ctxCode = ctxCode.replace(`"tabBar": {}`, `"tabBar": {} as TabBar`);
|
||||
ctxCode = ctxCode.replace(
|
||||
`"subPackages": []`,
|
||||
`"subPackages": [] as SubPackage[]`,
|
||||
);
|
||||
|
||||
code = code.replace("const ctx = {}", `const ctx = ${ctxCode}`);
|
||||
|
||||
code = code.replace(
|
||||
"const ctx = parse<Ctx>({})!",
|
||||
`const ctx = parse<Ctx>(${ctxCode})!`,
|
||||
);
|
||||
}
|
||||
|
||||
// if (id.includes("/cool/service/index.ts")) {
|
||||
// const eps = await createEps();
|
||||
|
||||
// if (eps.serviceCode) {
|
||||
// const { content, types } = eps.serviceCode;
|
||||
// const typeCode = `import type { ${uniq(types).join(", ")} } from '../types';`;
|
||||
|
||||
// code =
|
||||
// typeCode +
|
||||
// "\n\n" +
|
||||
// code.replace("const service = {}", `const service = ${content}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (id.endsWith(".json")) {
|
||||
const d = JSON.parse(code);
|
||||
|
||||
for (let i in d) {
|
||||
let k = i;
|
||||
|
||||
for (let j in SAFE_CHAR_MAP_LOCALE) {
|
||||
k = k.replaceAll(j, SAFE_CHAR_MAP_LOCALE[j]);
|
||||
}
|
||||
|
||||
if (k != i) {
|
||||
d[k] = d[i];
|
||||
delete d[i];
|
||||
}
|
||||
}
|
||||
|
||||
// 转字符串,不然会报错:Method too large
|
||||
if (id.includes("/locale/")) {
|
||||
let t: string[] = [];
|
||||
|
||||
(d as string[][]).forEach(([a, b]) => {
|
||||
t.push(`${a}<__=__>${b}`);
|
||||
});
|
||||
|
||||
code = JSON.stringify([[t.join("<__&__>")]]);
|
||||
} else {
|
||||
code = JSON.stringify(d);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
map: { mappings: "" },
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vite-cool-uniappx-code",
|
||||
transform(code, id) {
|
||||
if (id.endsWith(".json")) {
|
||||
return {
|
||||
code: code.replace("new UTSJSONObject", ""),
|
||||
map: { mappings: "" },
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 特殊字符映射表
|
||||
*/
|
||||
export const SAFE_CHAR_MAP: Record<string, string> = {
|
||||
"[": "-bracket-start-",
|
||||
"]": "-bracket-end-",
|
||||
"(": "-paren-start-",
|
||||
")": "-paren-end-",
|
||||
"{": "-brace-start-",
|
||||
"}": "-brace-end-",
|
||||
$: "-dollar-",
|
||||
"#": "-hash-",
|
||||
"!": "-important-",
|
||||
"/": "-slash-",
|
||||
":": "-colon-",
|
||||
};
|
||||
|
||||
/**
|
||||
* 特殊字符映射表(国际化)
|
||||
*/
|
||||
export const SAFE_CHAR_MAP_LOCALE: Record<string, string> = {
|
||||
"[": "-bracket-start-",
|
||||
"]": "-bracket-end-",
|
||||
"(": "-paren-start-",
|
||||
")": "-paren-end-",
|
||||
"{": "-brace-start-",
|
||||
"}": "-brace-end-",
|
||||
$: "-dollar-",
|
||||
"#": "-hash-",
|
||||
"!": "-important-",
|
||||
"/": "-slash-",
|
||||
":": "-colon-",
|
||||
" ": "-space-",
|
||||
"<": "-lt-",
|
||||
">": "-gt-",
|
||||
"&": "-amp-",
|
||||
"|": "-pipe-",
|
||||
"^": "-caret-",
|
||||
"~": "-tilde-",
|
||||
"`": "-backtick-",
|
||||
"'": "-single-quote-",
|
||||
".": "-dot-",
|
||||
"?": "-question-",
|
||||
"*": "-star-",
|
||||
"+": "-plus-",
|
||||
"-": "-dash-",
|
||||
_: "-underscore-",
|
||||
"=": "-equal-",
|
||||
"%": "-percent-",
|
||||
"@": "-at-",
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import { firstUpperCase } from "../utils";
|
||||
|
||||
/**
|
||||
* 将模板字符串扁平化处理,转换为 Service 类型定义
|
||||
* @param template - 包含 Service 类型定义的模板字符串
|
||||
* @returns 处理后的 Service 类型定义字符串
|
||||
* @throws {Error} 当模板中找不到 Service 类型定义时抛出错误
|
||||
*/
|
||||
export function flatten(template: string): string {
|
||||
// 查找 Service 类型定义的起始位置
|
||||
const startIndex = template.indexOf("export type Service = {");
|
||||
|
||||
// 保留 Service 类型定义前的内容
|
||||
let header = template.substring(0, startIndex);
|
||||
|
||||
// 获取 Service 类型定义及其内容,去除换行和制表符
|
||||
const serviceTemplateContent = template.substring(startIndex).replace(/\n|\t/g, "");
|
||||
|
||||
// 找到 Service 的内容部分
|
||||
const serviceStartIndex = serviceTemplateContent.indexOf("{") + 1;
|
||||
const serviceEndIndex = findClosingBrace(serviceTemplateContent, serviceStartIndex);
|
||||
const serviceInnerContent = serviceTemplateContent
|
||||
.substring(serviceStartIndex, serviceEndIndex)
|
||||
.trim();
|
||||
|
||||
// 存储所有接口定义
|
||||
const allInterfaces = new Map<string, string>();
|
||||
|
||||
// 处理 Service 内容,保持原有结构但替换嵌套对象为接口引用
|
||||
const serviceContent = buildCurrentLevelContent(serviceInnerContent);
|
||||
|
||||
// 递归收集所有需要生成的接口
|
||||
flattenContent(serviceInnerContent, allInterfaces, []);
|
||||
|
||||
// 生成所有接口定义
|
||||
let interfaces = "";
|
||||
allInterfaces.forEach((content, key) => {
|
||||
interfaces += `\nexport interface ${firstUpperCase(key)}Interface { ${content} }\n`;
|
||||
});
|
||||
|
||||
return `${header}${interfaces}\nexport type Service = { ${serviceContent} }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找匹配的右花括号位置
|
||||
* @param str - 要搜索的字符串
|
||||
* @param startIndex - 开始搜索的位置
|
||||
* @returns 匹配的右花括号位置
|
||||
* @throws {Error} 当找不到匹配的右花括号时抛出错误
|
||||
*/
|
||||
function findClosingBrace(str: string, startIndex: number): number {
|
||||
let braceCount = 1;
|
||||
let currentIndex = startIndex;
|
||||
|
||||
while (currentIndex < str.length && braceCount > 0) {
|
||||
if (str[currentIndex] === "{") braceCount++;
|
||||
if (str[currentIndex] === "}") braceCount--;
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
if (braceCount !== 0) {
|
||||
throw new Error("Unmatched braces in the template");
|
||||
}
|
||||
|
||||
return currentIndex - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集所有需要生成的接口
|
||||
* @param content - 要处理的内容
|
||||
* @param allInterfaces - 存储所有接口定义的 Map
|
||||
* @param parentFields - 父级字段数组(暂未使用)
|
||||
*/
|
||||
function flattenContent(
|
||||
content: string,
|
||||
allInterfaces: Map<string, string>,
|
||||
parentFields: string[],
|
||||
): void {
|
||||
const interfacePattern = /(\w+)\s*:\s*\{/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = interfacePattern.exec(content)) !== null) {
|
||||
const key = match[1];
|
||||
const startIndex = match.index + match[0].length;
|
||||
const endIndex = findClosingBrace(content, startIndex);
|
||||
|
||||
if (endIndex > startIndex) {
|
||||
const innerContent = content.substring(startIndex, endIndex).trim();
|
||||
|
||||
// 构建当前接口的内容,将嵌套对象替换为接口引用
|
||||
const currentLevelContent = buildCurrentLevelContent(innerContent);
|
||||
allInterfaces.set(key, currentLevelContent);
|
||||
|
||||
// 递归处理嵌套内容
|
||||
flattenContent(innerContent, allInterfaces, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建当前级别的内容,将嵌套对象替换为接口引用
|
||||
* @param content - 内容字符串
|
||||
* @returns 处理后的内容
|
||||
*/
|
||||
function buildCurrentLevelContent(content: string): string {
|
||||
const interfacePattern = /(\w+)\s*:\s*\{/g;
|
||||
let result = content;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// 重置正则表达式的 lastIndex
|
||||
interfacePattern.lastIndex = 0;
|
||||
|
||||
while ((match = interfacePattern.exec(content)) !== null) {
|
||||
const key = match[1];
|
||||
const startIndex = match.index + match[0].length;
|
||||
const endIndex = findClosingBrace(content, startIndex);
|
||||
|
||||
if (endIndex > startIndex) {
|
||||
const fullMatch = content.substring(match.index, endIndex + 1);
|
||||
const replacement = `${key}: ${firstUpperCase(key)}Interface;`;
|
||||
result = result.replace(fullMatch, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理多余的分号和空格
|
||||
result = result.replace(/;+/g, ";").replace(/\s+/g, " ").trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Plugin } from "vite";
|
||||
import { config } from "../config";
|
||||
import { tailwindPlugin } from "./tailwind";
|
||||
import { codePlugin } from "./code";
|
||||
|
||||
/**
|
||||
* uniappX 入口,自动注入 Tailwind 类名转换插件
|
||||
* @param options 配置项
|
||||
* @returns Vite 插件数组
|
||||
*/
|
||||
export async function uniappX() {
|
||||
const plugins: Plugin[] = [];
|
||||
|
||||
if (config.type == "uniapp-x") {
|
||||
plugins.push(...codePlugin());
|
||||
|
||||
if (config.tailwind.enable) {
|
||||
plugins.push(...tailwindPlugin());
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
// @ts-ignore
|
||||
import valueParser from "postcss-value-parser";
|
||||
import { config } from "../config";
|
||||
import type { Plugin } from "vite";
|
||||
import { SAFE_CHAR_MAP } from "./config";
|
||||
import {
|
||||
addScriptContent,
|
||||
getClassContent,
|
||||
getClassNames,
|
||||
getNodes,
|
||||
isTailwindClass,
|
||||
} from "./utils";
|
||||
|
||||
/**
|
||||
* 转换类名中的特殊字符为安全字符
|
||||
*/
|
||||
export function toSafeClass(className: string): string {
|
||||
if (config.utsPlatform == "web") {
|
||||
return className;
|
||||
}
|
||||
|
||||
if (className.includes(":host")) {
|
||||
return className;
|
||||
}
|
||||
|
||||
// 如果是表达式,则不进行转换
|
||||
if (["!=", "!==", "?", ":", "="].includes(className)) {
|
||||
return className;
|
||||
}
|
||||
|
||||
let safeClassName = className;
|
||||
|
||||
// 移除转义字符
|
||||
if (safeClassName.includes("\\")) {
|
||||
safeClassName = safeClassName.replace(/\\/g, "");
|
||||
}
|
||||
|
||||
// 处理暗黑模式
|
||||
if (safeClassName.includes(":is")) {
|
||||
if (safeClassName.includes(":is(.dark *)")) {
|
||||
safeClassName = safeClassName.replace(/:is\(.dark \*\)/g, "");
|
||||
if (safeClassName.startsWith(".dark:")) {
|
||||
const className = safeClassName.replace(/^\.dark:/, ".dark:");
|
||||
safeClassName = `${className}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 替换特殊字符
|
||||
for (const [char, replacement] of Object.entries(SAFE_CHAR_MAP)) {
|
||||
const regex = new RegExp("\\" + char, "g");
|
||||
if (regex.test(safeClassName)) {
|
||||
safeClassName = safeClassName.replace(regex, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return safeClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 RGB 为 RGBA 格式
|
||||
*/
|
||||
function rgbToRgba(rgbValue: string): string {
|
||||
const match = rgbValue.match(/rgb\(([\d\s]+)\/\s*([\d.]+)\)/);
|
||||
if (!match) return rgbValue;
|
||||
|
||||
const [, rgb, alpha] = match;
|
||||
const [r, g, b] = rgb.split(/\s+/);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
function remToRpx(remValue: string): string {
|
||||
const { remUnit = 14, remPrecision = 6, rpxRatio = 2 } = config.tailwind!;
|
||||
const conversionFactor = remUnit * rpxRatio;
|
||||
|
||||
const precision = (remValue.split(".")[1] || "").length;
|
||||
const rpxValue = (parseFloat(remValue) * conversionFactor)
|
||||
.toFixed(precision || remPrecision)
|
||||
.replace(/\.?0+$/, "");
|
||||
|
||||
return `${rpxValue}rpx`;
|
||||
}
|
||||
|
||||
/**
|
||||
* PostCSS 插件
|
||||
* 处理类名和单位转换
|
||||
*/
|
||||
function postcssPlugin(): Plugin {
|
||||
return {
|
||||
name: "vite-cool-uniappx-postcss",
|
||||
enforce: "pre",
|
||||
|
||||
config() {
|
||||
return {
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
{
|
||||
postcssPlugin: "vite-cool-uniappx-class-mapping",
|
||||
prepare() {
|
||||
return {
|
||||
// 处理选择器规则
|
||||
Rule(rule: any) {
|
||||
if (
|
||||
[
|
||||
".button-hover",
|
||||
":deep(",
|
||||
"&::",
|
||||
"uni-",
|
||||
".uni-",
|
||||
].some((e) => rule.selector.includes(e))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换选择器为安全的类名格式
|
||||
rule.selector = toSafeClass(rule.selector);
|
||||
},
|
||||
|
||||
// 处理声明规则
|
||||
Declaration(decl: any) {
|
||||
const className = decl.parent.selector || "";
|
||||
|
||||
if (!decl.parent._twValues) {
|
||||
decl.parent._twValues = {};
|
||||
}
|
||||
|
||||
// 处理 Tailwind 自定义属性
|
||||
if (decl.prop.includes("--tw-")) {
|
||||
decl.parent._twValues[decl.prop] =
|
||||
decl.value.includes("rem")
|
||||
? remToRpx(decl.value)
|
||||
: decl.value;
|
||||
|
||||
decl.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换 RGB 颜色为 RGBA 格式
|
||||
if (
|
||||
decl.value.includes("rgb(") &&
|
||||
decl.value.includes("/")
|
||||
) {
|
||||
decl.value = rgbToRgba(decl.value);
|
||||
}
|
||||
|
||||
// 处理文本大小相关样式
|
||||
if (
|
||||
decl.value.includes("rpx") &&
|
||||
decl.prop == "color" &&
|
||||
className.includes("text-")
|
||||
) {
|
||||
decl.prop = "font-size";
|
||||
}
|
||||
|
||||
// 删除不支持的属性
|
||||
if (["filter"].includes(decl.prop)) {
|
||||
decl.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理 flex-1
|
||||
if (decl.prop == "flex") {
|
||||
if (decl.value.startsWith("1")) {
|
||||
decl.value = "1";
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 vertical-align 属性
|
||||
if (decl.prop == "vertical-align") {
|
||||
decl.remove();
|
||||
}
|
||||
|
||||
// 处理 visibility 属性
|
||||
if (decl.prop == "visibility") {
|
||||
decl.remove();
|
||||
}
|
||||
|
||||
// 处理 sticky 属性
|
||||
if (className == ".sticky") {
|
||||
if (
|
||||
decl.prop == "position" ||
|
||||
decl.value == "sticky"
|
||||
) {
|
||||
decl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 解析声明值
|
||||
const parsed = valueParser(decl.value);
|
||||
let hasChanges = false;
|
||||
|
||||
// 遍历并处理声明值中的节点
|
||||
parsed.walk((node: any) => {
|
||||
// 处理单位转换(rem -> rpx)
|
||||
if (node.type === "word") {
|
||||
const unit = valueParser.unit(node.value);
|
||||
|
||||
if (typeof unit != "boolean") {
|
||||
if (unit?.unit === "rem") {
|
||||
node.value = remToRpx(unit.number);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 CSS 变量
|
||||
if (
|
||||
node.type === "function" &&
|
||||
node.value === "var"
|
||||
) {
|
||||
const twKey = node.nodes[0]?.value;
|
||||
|
||||
// 替换 Tailwind 变量为实际值
|
||||
if (twKey?.startsWith("--tw-")) {
|
||||
if (decl.parent._twValues) {
|
||||
node.type = "word";
|
||||
node.value =
|
||||
decl.parent._twValues[twKey] ||
|
||||
"none";
|
||||
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 更新声明值
|
||||
if (hasChanges) {
|
||||
decl.value = parsed.toString();
|
||||
}
|
||||
|
||||
// 移除 Tailwind 生成的无效 none 变换
|
||||
const nones = [
|
||||
"translate(none, none)",
|
||||
"rotate(none)",
|
||||
"skewX(none)",
|
||||
"skewY(none)",
|
||||
"scaleX(none)",
|
||||
"scaleY(none)",
|
||||
];
|
||||
|
||||
if (decl.value) {
|
||||
nones.forEach((noneStr) => {
|
||||
decl.value = decl.value.replace(noneStr, "");
|
||||
|
||||
if (!decl.value || !decl.value.trim()) {
|
||||
decl.value = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* uvue class 转换插件
|
||||
*/
|
||||
function transformPlugin(): Plugin {
|
||||
return {
|
||||
name: "vite-cool-uniappx-transform",
|
||||
enforce: "pre",
|
||||
|
||||
async transform(code, id) {
|
||||
const { darkTextClass } = config.tailwind!;
|
||||
|
||||
// 判断是否为 uvue 文件
|
||||
if (id.endsWith(".uvue") || id.includes(".uvue?type=page")) {
|
||||
// 避免影响到其他模块/插件
|
||||
if (id.includes("uni_modules/") && !id.includes("uni_modules/cool-")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let modifiedCode = code;
|
||||
|
||||
// 获取所有节点
|
||||
const nodes = getNodes(code);
|
||||
|
||||
// 遍历处理每个节点
|
||||
nodes.forEach((node) => {
|
||||
if (node.startsWith("<!--")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _node = node;
|
||||
|
||||
// uniappx 插件模式
|
||||
if (!config.uniapp.isPlugin) {
|
||||
// 为 text 节点添加暗黑模式文本颜色
|
||||
if (!_node.includes(darkTextClass) && _node.startsWith("<text")) {
|
||||
let classIndex = _node.indexOf("class=");
|
||||
|
||||
// 处理动态 class
|
||||
if (classIndex >= 0) {
|
||||
if (_node[classIndex - 1] == ":") {
|
||||
classIndex = _node.lastIndexOf("class=");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加暗黑模式类名
|
||||
if (classIndex >= 0) {
|
||||
_node =
|
||||
_node.substring(0, classIndex + 7) +
|
||||
`${darkTextClass} ` +
|
||||
_node.substring(classIndex + 7, _node.length);
|
||||
} else {
|
||||
_node =
|
||||
_node.substring(0, 5) +
|
||||
` class="${darkTextClass}" ` +
|
||||
_node.substring(5, _node.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有类名
|
||||
const classNames = getClassNames(_node);
|
||||
|
||||
// 转换 Tailwind 类名为安全类名
|
||||
classNames.forEach((name, index) => {
|
||||
if (isTailwindClass(name)) {
|
||||
const safeName = toSafeClass(name);
|
||||
_node = _node.replaceAll(name, safeName);
|
||||
classNames[index] = safeName;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否存在动态类名
|
||||
const hasDynamicClass = _node.includes(":class=");
|
||||
|
||||
// 如果没有动态类名,添加空的动态类名绑定
|
||||
if (!hasDynamicClass) {
|
||||
// 优化写法,避免重复字符串拼接
|
||||
const insertIndex = _node.length - (_node.endsWith("/>") ? 2 : 1);
|
||||
|
||||
_node =
|
||||
_node.slice(0, insertIndex) + ` :class="{}"` + _node.slice(insertIndex);
|
||||
}
|
||||
|
||||
// 获取暗黑模式类名
|
||||
let darkClassNames = classNames.filter(
|
||||
(name) => name.startsWith("dark-colon-") || name.startsWith("dark:"),
|
||||
);
|
||||
|
||||
// 插件模式,不支持 dark:
|
||||
if (config.uniapp.isPlugin) {
|
||||
darkClassNames = [];
|
||||
}
|
||||
|
||||
// 生成暗黑模式类名的动态绑定
|
||||
const darkClassContent = darkClassNames
|
||||
.map((name) => {
|
||||
_node = _node.replaceAll(name, "");
|
||||
return `'${name}': __isDark`;
|
||||
})
|
||||
.join(",");
|
||||
|
||||
// 获取所有 class 内容
|
||||
const classContents = getClassContent(_node);
|
||||
|
||||
// 处理对象形式的动态类名
|
||||
const dynamicClassContent_1 = classContents.find(
|
||||
(content) => content.startsWith("{") && content.endsWith("}"),
|
||||
);
|
||||
|
||||
if (dynamicClassContent_1) {
|
||||
const v =
|
||||
dynamicClassContent_1[0] +
|
||||
(darkClassContent ? `${darkClassContent},` : "") +
|
||||
dynamicClassContent_1.substring(1);
|
||||
|
||||
_node = _node.replaceAll(dynamicClassContent_1, v);
|
||||
}
|
||||
|
||||
// 处理数组形式的动态类名
|
||||
const dynamicClassContent_2 = classContents.find(
|
||||
(content) => content.startsWith("[") && content.endsWith("]"),
|
||||
);
|
||||
|
||||
if (dynamicClassContent_2) {
|
||||
const v =
|
||||
dynamicClassContent_2[0] +
|
||||
`{${darkClassContent}},` +
|
||||
dynamicClassContent_2.substring(1);
|
||||
|
||||
_node = _node.replaceAll(dynamicClassContent_2, v);
|
||||
}
|
||||
|
||||
// 更新节点内容
|
||||
modifiedCode = modifiedCode.replace(node, _node);
|
||||
});
|
||||
|
||||
// 如果代码有修改
|
||||
if (modifiedCode !== code) {
|
||||
// 添加暗黑模式依赖
|
||||
if (modifiedCode.includes("__isDark")) {
|
||||
if (!modifiedCode.includes("<script")) {
|
||||
modifiedCode += '<script lang="ts" setup></script>';
|
||||
}
|
||||
|
||||
if (!config.uniapp.isPlugin) {
|
||||
modifiedCode = addScriptContent(
|
||||
modifiedCode,
|
||||
"\nimport { isDark as __isDark } from '@/cool';",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空的类名绑定
|
||||
modifiedCode = modifiedCode
|
||||
.replaceAll(':class="{}"', "")
|
||||
.replaceAll('class=""', "")
|
||||
.replaceAll('class=" "', "");
|
||||
|
||||
return {
|
||||
code: modifiedCode,
|
||||
map: { mappings: "" },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Tailwind 类名转换插件
|
||||
*/
|
||||
export function tailwindPlugin() {
|
||||
return [postcssPlugin(), transformPlugin()];
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
/**
|
||||
* 获取动态类名
|
||||
*/
|
||||
export const getDynamicClassNames = (value: string): string[] => {
|
||||
const names = new Set<string>();
|
||||
|
||||
// 匹配函数调用中的对象参数(如 parseClass({'!bg-surface-50': hoverable}))
|
||||
const functionCallRegex = /\w+\s*\(\s*\{([^}]*)\}\s*\)/gs;
|
||||
let funcMatch;
|
||||
while ((funcMatch = functionCallRegex.exec(value)) !== null) {
|
||||
const objContent = funcMatch[1];
|
||||
// 提取对象中的键
|
||||
const keyRegex = /['"](.*?)['"]\s*:/gs;
|
||||
let keyMatch;
|
||||
while ((keyMatch = keyRegex.exec(objContent)) !== null) {
|
||||
keyMatch[1].trim() && names.add(keyMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配对象键(如 { 'text-a': 1 })- 优化版本,避免跨行错误匹配
|
||||
const objKeyRegex = /[{,]\s*['"](.*?)['"]\s*:/gs;
|
||||
let objKeyMatch;
|
||||
while ((objKeyMatch = objKeyRegex.exec(value)) !== null) {
|
||||
const className = objKeyMatch[1].trim();
|
||||
// 确保是有效的CSS类名,避免匹配到错误内容
|
||||
if (className && !className.includes("\n") && !className.includes("\t")) {
|
||||
names.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配数组中的字符串元素(如 'text-center')- 优化版本
|
||||
const arrayStringRegex = /(?:^|[,\[\s])\s*['"](.*?)['"]/gs;
|
||||
let arrayMatch;
|
||||
while ((arrayMatch = arrayStringRegex.exec(value)) !== null) {
|
||||
const className = arrayMatch[1].trim();
|
||||
// 确保是有效的CSS类名
|
||||
if (className && !className.includes("\n") && !className.includes("\t")) {
|
||||
names.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配三元表达式中的字符串(如 'dark' 和 'light')
|
||||
const ternaryRegex = /(\?|:)\s*['"](.*?)['"]/gs;
|
||||
let ternaryMatch;
|
||||
while ((ternaryMatch = ternaryRegex.exec(value)) !== null) {
|
||||
ternaryMatch[2].trim() && names.add(ternaryMatch[2]);
|
||||
}
|
||||
|
||||
// 匹配反引号模板字符串 - 改进版本
|
||||
const templateRegex = /`([^`]*)`/gs;
|
||||
let templateMatch;
|
||||
while ((templateMatch = templateRegex.exec(value)) !== null) {
|
||||
const templateContent = templateMatch[1];
|
||||
|
||||
// 提取模板字符串中的普通文本部分(排除 ${} 表达式)
|
||||
const textParts = templateContent.split(/\$\{[^}]*\}/);
|
||||
textParts.forEach((part) => {
|
||||
part.trim()
|
||||
.split(/\s+/)
|
||||
.forEach((className) => {
|
||||
className.trim() && names.add(className.trim());
|
||||
});
|
||||
});
|
||||
|
||||
// 提取模板字符串中 ${} 表达式内的字符串
|
||||
const expressionRegex = /\$\{([^}]*)\}/gs;
|
||||
let expressionMatch;
|
||||
while ((expressionMatch = expressionRegex.exec(templateContent)) !== null) {
|
||||
const expression = expressionMatch[1];
|
||||
// 递归处理表达式中的动态类名
|
||||
getDynamicClassNames(expression).forEach((name) => names.add(name));
|
||||
}
|
||||
}
|
||||
|
||||
// 处理混合字符串(模板字符串 + 普通文本),如 "`text-red-900` text-red-1000"
|
||||
const mixedStringRegex = /`[^`]*`\s+([a-zA-Z0-9\-_\s]+)/g;
|
||||
let mixedMatch;
|
||||
while ((mixedMatch = mixedStringRegex.exec(value)) !== null) {
|
||||
const additionalClasses = mixedMatch[1].trim().split(/\s+/);
|
||||
additionalClasses.forEach((className) => {
|
||||
className.trim() && names.add(className.trim());
|
||||
});
|
||||
}
|
||||
|
||||
// 处理普通字符串,多个类名用空格分割
|
||||
const stringRegex = /['"]([\w\s\-!:\/]+?)['"]/gs;
|
||||
let stringMatch;
|
||||
while ((stringMatch = stringRegex.exec(value)) !== null) {
|
||||
const classNames = stringMatch[1].trim().split(/\s+/);
|
||||
classNames.forEach((className) => {
|
||||
className.trim() && names.add(className.trim());
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(names);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取类名
|
||||
*/
|
||||
export function getClassNames(code: string): string[] {
|
||||
// 修改正则表达式以支持多行匹配,避免内层引号冲突
|
||||
const classRegex =
|
||||
/(?:class|:class|:pt|:hover-class)\s*=\s*(['"`])((?:[^'"`\\]|\\.|`[^`]*`|'[^']*'|"[^"]*")*?)\1/gis;
|
||||
const classNames = new Set<string>();
|
||||
let match;
|
||||
|
||||
while ((match = classRegex.exec(code)) !== null) {
|
||||
const attribute = match[0].split("=")[0].trim();
|
||||
const isStaticClass = attribute === "class" || attribute === "hover-class";
|
||||
const isPtAttribute = attribute.includes("pt");
|
||||
const value = match[2].trim();
|
||||
|
||||
if (isStaticClass) {
|
||||
// 处理静态 class 和 hover-class
|
||||
value.split(/\s+/).forEach((name) => name && classNames.add(name));
|
||||
} else if (isPtAttribute) {
|
||||
// 处理 :pt 属性中的 className
|
||||
parseClasNameFromPt(value, classNames);
|
||||
} else {
|
||||
// 处理动态 :class 和 :hover-class
|
||||
getDynamicClassNames(value).forEach((name) => classNames.add(name));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(classNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 :pt 属性中解析 className
|
||||
*/
|
||||
function parseClasNameFromPt(value: string, classNames: Set<string>) {
|
||||
// 递归查找所有 className 属性
|
||||
const classNameRegex = /className\s*:\s*/g;
|
||||
let match;
|
||||
|
||||
while ((match = classNameRegex.exec(value)) !== null) {
|
||||
const startPos = match.index + match[0].length;
|
||||
const classNameValue = extractComplexValue(value, startPos);
|
||||
|
||||
if (classNameValue) {
|
||||
// 如果是字符串字面量
|
||||
if (
|
||||
classNameValue.startsWith('"') ||
|
||||
classNameValue.startsWith("'") ||
|
||||
classNameValue.startsWith("`")
|
||||
) {
|
||||
if (classNameValue.startsWith("`")) {
|
||||
// 处理模板字符串
|
||||
getDynamicClassNames(classNameValue).forEach((name) => classNames.add(name));
|
||||
} else {
|
||||
// 处理普通字符串
|
||||
const strMatch = classNameValue.match(/['"](.*?)['"]/);
|
||||
if (strMatch) {
|
||||
strMatch[1].split(/\s+/).forEach((name) => name && classNames.add(name));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 处理动态值(如函数调用、对象等)
|
||||
getDynamicClassNames(classNameValue).forEach((name) => classNames.add(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取复杂值(支持嵌套引号和括号)
|
||||
*/
|
||||
function extractComplexValue(text: string, startPos: number): string | null {
|
||||
let pos = startPos;
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let stringChar = "";
|
||||
let result = "";
|
||||
|
||||
// 跳过开头的空白字符
|
||||
while (pos < text.length && /\s/.test(text[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
while (pos < text.length) {
|
||||
const char = text[pos];
|
||||
|
||||
if (!inString) {
|
||||
if (char === '"' || char === "'" || char === "`") {
|
||||
inString = true;
|
||||
stringChar = char;
|
||||
result += char;
|
||||
} else if (char === "{" || char === "(" || char === "[") {
|
||||
depth++;
|
||||
result += char;
|
||||
} else if (char === "}" || char === ")" || char === "]") {
|
||||
if (depth === 0 && char === "}") {
|
||||
// 遇到顶层的 } 时结束
|
||||
break;
|
||||
}
|
||||
depth--;
|
||||
result += char;
|
||||
} else if (char === "," && depth === 0) {
|
||||
// 遇到顶层的逗号时结束
|
||||
break;
|
||||
} else if (char === "\n" && depth === 0 && result.trim() !== "") {
|
||||
// 如果遇到换行且不在嵌套结构中,且已有内容,则结束
|
||||
break;
|
||||
} else {
|
||||
result += char;
|
||||
}
|
||||
} else {
|
||||
result += char;
|
||||
if (char === stringChar && text[pos - 1] !== "\\") {
|
||||
inString = false;
|
||||
stringChar = "";
|
||||
|
||||
// 如果字符串结束且depth为0,检查是否应该结束
|
||||
if (depth === 0) {
|
||||
// 看看下一个非空白字符是什么
|
||||
let nextPos = pos + 1;
|
||||
while (nextPos < text.length && /\s/.test(text[nextPos])) {
|
||||
nextPos++;
|
||||
}
|
||||
if (nextPos < text.length && (text[nextPos] === "," || text[nextPos] === "}")) {
|
||||
// 如果下一个字符是逗号或右括号,则结束
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
return result.trim() || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 class 内容
|
||||
*/
|
||||
export function getClassContent(code: string) {
|
||||
// 修改正则表达式以支持多行匹配,避免内层引号冲突
|
||||
const regex =
|
||||
/(?:class|:class|:pt|:hover-class)\s*=\s*(['"`])((?:[^'"`\\]|\\.|`[^`]*`|'[^']*'|"[^"]*")*?)\1/gis;
|
||||
const texts: string[] = [];
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
const attribute = match[0].split("=")[0].trim();
|
||||
const isPtAttribute = attribute.includes("pt");
|
||||
const value = match[2];
|
||||
|
||||
if (isPtAttribute) {
|
||||
// 手动解析 className 值
|
||||
const classNameRegex = /className\s*:\s*/g;
|
||||
let classNameMatchResult;
|
||||
while ((classNameMatchResult = classNameRegex.exec(value)) !== null) {
|
||||
const startPos = classNameMatchResult.index + classNameMatchResult[0].length;
|
||||
const classNameValue = extractComplexValue(value, startPos);
|
||||
if (classNameValue) {
|
||||
texts.push(classNameValue);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
texts.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return texts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点
|
||||
*/
|
||||
export function getNodes(code: string) {
|
||||
const nodes: string[] = [];
|
||||
|
||||
// 找到所有顶级template标签的完整内容
|
||||
function findTemplateContents(content: string): string[] {
|
||||
const results: string[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < content.length) {
|
||||
const templateStart = content.indexOf("<template", index);
|
||||
if (templateStart === -1) break;
|
||||
|
||||
// 找到模板标签的结束位置
|
||||
const tagEnd = content.indexOf(">", templateStart);
|
||||
if (tagEnd === -1) break;
|
||||
|
||||
// 使用栈来匹配配对的template标签
|
||||
let stack = 1;
|
||||
let currentPos = tagEnd + 1;
|
||||
|
||||
while (currentPos < content.length && stack > 0) {
|
||||
const nextTemplateStart = content.indexOf("<template", currentPos);
|
||||
const nextTemplateEnd = content.indexOf("</template>", currentPos);
|
||||
|
||||
if (nextTemplateEnd === -1) break;
|
||||
|
||||
// 如果开始标签更近,说明有嵌套
|
||||
if (nextTemplateStart !== -1 && nextTemplateStart < nextTemplateEnd) {
|
||||
// 找到开始标签的完整结束
|
||||
const nestedTagEnd = content.indexOf(">", nextTemplateStart);
|
||||
if (nestedTagEnd !== -1) {
|
||||
stack++;
|
||||
currentPos = nestedTagEnd + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 找到结束标签
|
||||
stack--;
|
||||
currentPos = nextTemplateEnd + 11; // '</template>'.length
|
||||
}
|
||||
}
|
||||
|
||||
if (stack === 0) {
|
||||
// 提取template内容(不包括template标签本身)
|
||||
const templateContent = content.substring(tagEnd + 1, currentPos - 11);
|
||||
results.push(templateContent);
|
||||
index = currentPos;
|
||||
} else {
|
||||
// 如果没有找到匹配的结束标签,跳过这个开始标签
|
||||
index = tagEnd + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// 递归提取所有template内容中的节点
|
||||
function extractNodesFromContent(content: string): void {
|
||||
// 先提取当前内容中的所有标签
|
||||
const regex = /<([^>]+)>/g;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
if (!match[1].startsWith("/") && !match[1].startsWith("template")) {
|
||||
nodes.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理嵌套的template
|
||||
const nestedTemplates = findTemplateContents(content);
|
||||
nestedTemplates.forEach((templateContent) => {
|
||||
extractNodesFromContent(templateContent);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有顶级template内容
|
||||
const templateContents = findTemplateContents(code);
|
||||
|
||||
// 处理每个template内容
|
||||
templateContents.forEach((templateContent) => {
|
||||
extractNodesFromContent(templateContent);
|
||||
});
|
||||
|
||||
return nodes.map((e) => `<${e}>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 script 标签内容
|
||||
*/
|
||||
export function addScriptContent(code: string, content: string) {
|
||||
const scriptMatch = /<script\b[^>]*>([\s\S]*?)<\/script>/g.exec(code);
|
||||
|
||||
if (!scriptMatch) {
|
||||
return code;
|
||||
}
|
||||
|
||||
const scriptContent = scriptMatch[1];
|
||||
const scriptStartIndex = scriptMatch.index + scriptMatch[0].indexOf(">") + 1;
|
||||
const scriptEndIndex = scriptStartIndex + scriptContent.length;
|
||||
|
||||
return (
|
||||
code.substring(0, scriptStartIndex) +
|
||||
"\n" +
|
||||
content +
|
||||
"\n" +
|
||||
scriptContent.trim() +
|
||||
code.substring(scriptEndIndex)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 Tailwind 类名
|
||||
*/
|
||||
export function isTailwindClass(className: string): boolean {
|
||||
const prefixes = [
|
||||
// 布局
|
||||
"container",
|
||||
"flex",
|
||||
"grid",
|
||||
"block",
|
||||
"inline",
|
||||
"hidden",
|
||||
"visible",
|
||||
|
||||
// 间距
|
||||
"p-",
|
||||
"px-",
|
||||
"py-",
|
||||
"pt-",
|
||||
"pr-",
|
||||
"pb-",
|
||||
"pl-",
|
||||
"m-",
|
||||
"mx-",
|
||||
"my-",
|
||||
"mt-",
|
||||
"mr-",
|
||||
"mb-",
|
||||
"ml-",
|
||||
"space-",
|
||||
"gap-",
|
||||
|
||||
// 尺寸
|
||||
"w-",
|
||||
"h-",
|
||||
"min-w-",
|
||||
"max-w-",
|
||||
"min-h-",
|
||||
"max-h-",
|
||||
|
||||
// 颜色
|
||||
"bg-",
|
||||
"text-",
|
||||
"border-",
|
||||
"ring-",
|
||||
"shadow-",
|
||||
|
||||
// 边框
|
||||
"border",
|
||||
"rounded",
|
||||
"ring",
|
||||
|
||||
// 字体
|
||||
"font-",
|
||||
"text-",
|
||||
"leading-",
|
||||
"tracking-",
|
||||
"antialiased",
|
||||
|
||||
// 定位
|
||||
"absolute",
|
||||
"relative",
|
||||
"fixed",
|
||||
"sticky",
|
||||
"static",
|
||||
"top-",
|
||||
"right-",
|
||||
"bottom-",
|
||||
"left-",
|
||||
"inset-",
|
||||
"z-",
|
||||
|
||||
// 变换
|
||||
"transform",
|
||||
"translate-",
|
||||
"rotate-",
|
||||
"scale-",
|
||||
"skew-",
|
||||
|
||||
// 过渡
|
||||
"transition",
|
||||
"duration-",
|
||||
"ease-",
|
||||
"delay-",
|
||||
|
||||
// 交互
|
||||
"cursor-",
|
||||
"select-",
|
||||
"pointer-events-",
|
||||
|
||||
// 溢出
|
||||
"overflow-",
|
||||
"truncate",
|
||||
|
||||
// 滚动
|
||||
"scroll-",
|
||||
|
||||
// 伪类和响应式
|
||||
"hover:",
|
||||
"focus:",
|
||||
"active:",
|
||||
"disabled:",
|
||||
"group-hover:",
|
||||
];
|
||||
|
||||
const statePrefixes = ["dark:", "dark:!", "light:", "sm:", "md:", "lg:", "xl:", "2xl:"];
|
||||
|
||||
if (className.startsWith("!") && !className.includes("!=")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const statePrefix of statePrefixes) {
|
||||
if (className.startsWith(statePrefix + prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 interface 转换为 type
|
||||
*/
|
||||
export function interfaceToType(code: string) {
|
||||
// 匹配 interface 定义
|
||||
const interfaceRegex = /interface\s+(\w+)(\s*extends\s+\w+)?\s*\{([^}]*)\}/g;
|
||||
|
||||
// 将 interface 转换为 type
|
||||
return code.replace(interfaceRegex, (match, name, extends_, content) => {
|
||||
// 处理可能存在的 extends
|
||||
const extendsStr = extends_ ? extends_ : "";
|
||||
|
||||
// 返回转换后的 type 定义
|
||||
return `type ${name}${extendsStr} = {${content}}`;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user