diff --git a/src/main/java/com/crzsc/plugin/utils/Constants.kt b/src/main/java/com/crzsc/plugin/utils/Constants.kt index 5fb859b..e28c63e 100644 --- a/src/main/java/com/crzsc/plugin/utils/Constants.kt +++ b/src/main/java/com/crzsc/plugin/utils/Constants.kt @@ -4,9 +4,6 @@ object Constants { /** 配置map的key */ const val KEY_CONFIGURATION_MAP = "flutter_assets_generator" - /** 是否启用插件能力 */ - const val KEY_ENABLE = "enable" - /** 输出目录的key */ const val KEY_OUTPUT_DIR = "output_dir" @@ -25,11 +22,8 @@ object Constants { /** 是否在 legacy 样式中使用父目录作为变量名前缀 (默认 true) */ const val KEY_NAMED_WITH_PARENT = "named_with_parent" - /** 命名风格 */ - const val KEY_NAME_STYLE = "name_style" - - /** 引用类型:class(包装类)或 string(原始路径) */ - const val KEY_LEAF_TYPE = "leaf_type" + /** 是否使用驼峰命名法 (默认 true) */ + const val KEY_NAMED_USE_CAMEL_CASE = "named_use_camel_case" /** 输出的文件名 */ const val KEY_OUTPUT_FILENAME = "output_filename" @@ -43,12 +37,6 @@ object Constants { /** 默认目录 */ const val DEFAULT_OUTPUT_DIR = "generated" const val DEFAULT_CLASS_NAME = "Assets" - const val DEFAULT_NAME_STYLE = "camel" - const val DEFAULT_LEAF_TYPE = "class" - const val NAME_STYLE_CAMEL = "camel" - const val NAME_STYLE_SNAKE = "snake" - const val LEAF_TYPE_CLASS = "class" - const val LEAF_TYPE_STRING = "string" const val DEFAULT_FILENAME_SPLIT_PATTERN = "[-_]" } diff --git a/src/main/java/com/crzsc/plugin/utils/DartClassGenerator.kt b/src/main/java/com/crzsc/plugin/utils/DartClassGenerator.kt index 1dcda5e..b60ce17 100644 --- a/src/main/java/com/crzsc/plugin/utils/DartClassGenerator.kt +++ b/src/main/java/com/crzsc/plugin/utils/DartClassGenerator.kt @@ -3,7 +3,6 @@ package com.crzsc.plugin.utils import com.crzsc.plugin.utils.PluginUtils.toLowCamelCase import com.intellij.openapi.diagnostic.Logger import java.text.Normalizer -import java.util.Locale /** Dart 代码生成器 负责将资源树转换为 Dart 类结构 */ class DartClassGenerator( @@ -25,10 +24,6 @@ class DartClassGenerator( // 用于收集额外的类(目录类),它们将被追加到主类之后 private val directoryClasses = StringBuilder() - private val generatedDirectoryClassNames = mutableMapOf() - private val usedDirectoryClassNames = mutableSetOf() - private val isStringLeafMode = - FileHelperNew.getLeafType(config) == Constants.LEAF_TYPE_STRING fun generate(): String { val style = FileHelperNew.getGenerationStyle(config) @@ -41,45 +36,33 @@ class DartClassGenerator( } } - private fun appendGeneratedFileHeader() { - buffer.append( - "///This file is automatically generated. DO NOT EDIT, all your changes would be lost.\n" - ) - buffer.append( - "///For AI assistants: read the project usage guide before changing asset access code.\n" - ) - buffer.append( - "///Docs: https://github.com/cr1992/FlutterAssetsGenerator\n" - ) - buffer.append("// ignore_for_file: dangling_library_doc_comments, implementation_imports\n") - } - private fun generateRobust(): String { LOG.info( "[FlutterAssetsGenerator #DartClassGenerator] [DartClassGenerator] Starting robust code generation..." ) - appendGeneratedFileHeader() + buffer.append( + "///This file is automatically generated. DO NOT EDIT, all your changes would be lost.\n" + ) + buffer.append("// ignore_for_file: dangling_library_doc_comments, implementation_imports\n") val className = FileHelperNew.getGeneratedClassName(config) LOG.info( "[FlutterAssetsGenerator #DartClassGenerator] [DartClassGenerator] Root class name: $className" ) // 生成 Imports - if (!isStringLeafMode) { - buffer.append("import 'package:flutter/widgets.dart';\n") - if (hasSvgDep) { - buffer.append("import 'package:flutter_svg/flutter_svg.dart';\n") - } - if (hasLottieDep) { - buffer.append("import 'package:lottie/lottie.dart';\n") - buffer.append("import 'package:lottie/src/lottie_builder.dart';\n") - buffer.append("import 'package:lottie/src/composition.dart';\n") - } - if (hasRiveDep) { - buffer.append("import 'package:rive/rive.dart' hide Image;\n") - } - buffer.append("\n") + buffer.append("import 'package:flutter/widgets.dart';\n") + if (hasSvgDep) { + buffer.append("import 'package:flutter_svg/flutter_svg.dart';\n") + } + if (hasLottieDep) { + buffer.append("import 'package:lottie/lottie.dart';\n") + buffer.append("import 'package:lottie/src/lottie_builder.dart';\n") + buffer.append("import 'package:lottie/src/composition.dart';\n") } + if (hasRiveDep) { + buffer.append("import 'package:rive/rive.dart' hide Image;\n") + } + buffer.append("\n") // 生成主类 (Root Class) val rootClassName = className @@ -117,9 +100,7 @@ class DartClassGenerator( buffer.append(directoryClasses) // 生成辅助类 (AssetGenImage 等) - if (!isStringLeafMode) { - generateHelperClasses(packageName) - } + generateHelperClasses(packageName) return buffer.toString() } @@ -128,27 +109,10 @@ class DartClassGenerator( LOG.info( "[FlutterAssetsGenerator #DartClassGenerator] Starting legacy code generation (camel_case)..." ) - appendGeneratedFileHeader() - - val isPackageParameterEnabled = FileHelperNew.isPackageParameterEnabled(config) - val packageName = if (isPackageParameterEnabled) config.map["name"] as? String else null - - if (!isStringLeafMode) { - buffer.append("import 'package:flutter/widgets.dart';\n") - if (hasSvgDep) { - buffer.append("import 'package:flutter_svg/flutter_svg.dart';\n") - } - if (hasLottieDep) { - buffer.append("import 'package:lottie/lottie.dart';\n") - buffer.append("import 'package:lottie/src/lottie_builder.dart';\n") - buffer.append("import 'package:lottie/src/composition.dart';\n") - } - if (hasRiveDep) { - buffer.append("import 'package:rive/rive.dart' hide Image;\n") - } - buffer.append("\n") - } - + buffer.append( + "///This file is automatically generated. DO NOT EDIT, all your changes would be lost.\n" + ) + buffer.append("// ignore_for_file: dangling_library_doc_comments, implementation_imports\n") val className = FileHelperNew.getGeneratedClassName(config) buffer.append("class $className {\n") @@ -159,24 +123,27 @@ class DartClassGenerator( collectFiles(rootNode, allFiles) val isNamedWithParent = FileHelperNew.isNamedWithParent(config) - val nameStyle = FileHelperNew.getNameStyle(config) val nameMap = mutableMapOf() + // 使用统一的特殊字符处理逻辑 for (file in allFiles) { if (file.type == MediaType.DIRECTORY) continue - val parent = file.virtualFile?.parent - val hasParentPrefix = isNamedWithParent && parent != null - var assetName = getSafeName(file.name, allowLeadingDigit = hasParentPrefix) + // 使用 getSafeName 处理文件名 (统一了特殊字符和驼峰转换逻辑) + // 注意: getSafeName 内部已经处理了 keywords 和 camelCase 转换 + var assetName = getSafeName(file.name) - if (hasParentPrefix) { - val parentName = getSafeName(parent!!.name) - assetName = combineIdentifier(parentName, assetName, nameStyle) + if (isNamedWithParent) { + file.virtualFile?.parent?.let { parent -> + val parentName = getSafeName(parent.name) + assetName = "${parentName}${assetName.replaceFirstChar { it.uppercase() }}" - if (nameMap.containsKey(assetName)) { - parent.parent?.let { parentParent -> - val parentParentName = getSafeName(parentParent.name) - assetName = combineIdentifier(parentParentName, assetName, nameStyle) + if (nameMap.containsKey(assetName)) { + parent.parent?.let { parentParent -> + val parentParentName = getSafeName(parentParent.name) + assetName = + "${parentParentName}${assetName.replaceFirstChar { it.uppercase() }}" + } } } } @@ -190,33 +157,18 @@ class DartClassGenerator( } nameMap[finalName] = 1 - val fullPath = "$prefix${file.path}" - val leafPath = buildLeafPath(fullPath) - if (!isStringLeafMode) { - when (file.type) { - MediaType.IMAGE -> - buffer.append(" static const AssetGenImage $finalName = AssetGenImage('$fullPath');\n") - MediaType.SVG -> - buffer.append(" static const SvgGenImage $finalName = SvgGenImage('$fullPath');\n") - MediaType.LOTTIE -> - buffer.append(" static const LottieGenImage $finalName = LottieGenImage('$fullPath');\n") - MediaType.RIVE -> - buffer.append(" static const RiveGenImage $finalName = RiveGenImage('$fullPath');\n") - else -> - buffer.append(" static const String $finalName = '$leafPath';\n") + var fullPath = "$prefix${file.path}" + val isPackageParameterEnabled = FileHelperNew.isPackageParameterEnabled(config) + if (isPackageParameterEnabled) { + val packageName = config.map["name"] as? String + if (packageName != null) { + fullPath = "packages/$packageName/$fullPath" } - } else { - buffer.append(" static const String $finalName = '$leafPath';\n") } + buffer.append(" static const String $assetName = '$fullPath';\n") } buffer.append("}\n") - - if (!isStringLeafMode) { - buffer.append("\n") - generateHelperClasses(packageName) - } - return buffer.toString() } @@ -304,12 +256,11 @@ class DartClassGenerator( val sameBaseNameFiles = baseNameCount[baseName] ?: emptyList() if (sameBaseNameFiles.size > 1) { // 有多个同名文件,添加扩展名(驼峰式) - val ext = - child.virtualFile?.extension?.let { - getSafeName(it, allowLeadingDigit = true) - } ?: "" + val ext = child.virtualFile?.extension?.let { getSafeName(it) } ?: "" if (ext.isNotEmpty()) { - uniqueName = appendExtensionSuffix(baseName, ext) + // 将扩展名首字母大写,形成驼峰式 + val extCamelCase = ext.replaceFirstChar { it.uppercase() } + uniqueName = "${baseName}${extCamelCase}" LOG.info( "[FlutterAssetsGenerator #DartClassGenerator] [generateNodeFields] Renaming ${child.name}: $baseName → $uniqueName" ) @@ -354,46 +305,40 @@ class DartClassGenerator( // 目录类的生成在 generate() 方法中统一处理 } else { + // child.path 已经包含完整的相对路径,加上包名前缀即可 val fullPath = "$prefix${child.path}" - val leafPath = buildLeafPath(fullPath) // 根据文件类型生成不同类型的字段 // 根类使用 static const,子目录类使用 final - if (isStringLeafMode) { - buffer.append( - " ${if (isRoot) "static const" else "final"} String $fieldName = '$leafPath';\n" - ) - } else { - when (child.type) { - MediaType.IMAGE -> { - buffer.append( - " ${if (isRoot) "static const" else "final"} AssetGenImage $fieldName = ${if (isRoot) "" else "const "}AssetGenImage('$fullPath');\n" - ) - } + when (child.type) { + MediaType.IMAGE -> { + buffer.append( + " ${if (isRoot) "static const" else "final"} AssetGenImage $fieldName = ${if (isRoot) "" else "const "}AssetGenImage('$fullPath');\n" + ) + } - MediaType.SVG -> { - buffer.append( - " ${if (isRoot) "static const" else "final"} SvgGenImage $fieldName = ${if (isRoot) "" else "const "}SvgGenImage('$fullPath');\n" - ) - } + MediaType.SVG -> { + buffer.append( + " ${if (isRoot) "static const" else "final"} SvgGenImage $fieldName = ${if (isRoot) "" else "const "}SvgGenImage('$fullPath');\n" + ) + } - MediaType.LOTTIE -> { - buffer.append( - " ${if (isRoot) "static const" else "final"} LottieGenImage $fieldName = ${if (isRoot) "" else "const "}LottieGenImage('$fullPath');\n" - ) - } + MediaType.LOTTIE -> { + buffer.append( + " ${if (isRoot) "static const" else "final"} LottieGenImage $fieldName = ${if (isRoot) "" else "const "}LottieGenImage('$fullPath');\n" + ) + } - MediaType.RIVE -> { - buffer.append( - " ${if (isRoot) "static const" else "final"} RiveGenImage $fieldName = ${if (isRoot) "" else "const "}RiveGenImage('$fullPath');\n" - ) - } + MediaType.RIVE -> { + buffer.append( + " ${if (isRoot) "static const" else "final"} RiveGenImage $fieldName = ${if (isRoot) "" else "const "}RiveGenImage('$fullPath');\n" + ) + } - else -> { - buffer.append( - " ${if (isRoot) "static const" else "final"} String $fieldName = '$fullPath';\n" - ) - } + else -> { + buffer.append( + " ${if (isRoot) "static const" else "final"} String $fieldName = '$fullPath';\n" + ) } } } @@ -463,9 +408,7 @@ class DartClassGenerator( /** 判断是否应该扁平化根节点 当根节点只有一个目录子节点时,应该扁平化 */ private fun shouldFlattenRoot(node: AssetNode): Boolean { - return node.children.size == 1 && - node.children[0].type == MediaType.DIRECTORY && - node.children[0].name == "assets" + return node.children.size == 1 && node.children[0].type == MediaType.DIRECTORY } // 存储基础包名前缀,用于所有目录类 @@ -538,12 +481,11 @@ class DartClassGenerator( val sameBaseNameFiles = baseNameCount[baseName] ?: emptyList() if (sameBaseNameFiles.size > 1) { // 有多个同名文件,添加扩展名(驼峰式) - val ext = - child.virtualFile?.extension?.let { - getSafeName(it, allowLeadingDigit = true) - } ?: "" + val ext = child.virtualFile?.extension?.let { getSafeName(it) } ?: "" if (ext.isNotEmpty()) { - uniqueName = appendExtensionSuffix(baseName, ext) + // 将扩展名首字母大写,形成驼峰式 + val extCamelCase = ext.replaceFirstChar { it.uppercase() } + uniqueName = "${baseName}${extCamelCase}" LOG.info( "[FlutterAssetsGenerator #DartClassGenerator] [generateDirectoryFields] Renaming ${child.name}: $baseName → $uniqueName" ) @@ -577,41 +519,37 @@ class DartClassGenerator( // 子目录类的生成已在 generateDirectoryClass 中递归处理 } else { + // child.path 已经包含完整的相对路径,加上包名前缀即可 val fullPath = "$prefix${child.path}" - val leafPath = buildLeafPath(fullPath) // 文件类型字段,使用 final - if (isStringLeafMode) { - sb.append(" final String $fieldName = '$leafPath';\n") - } else { - when (child.type) { - MediaType.IMAGE -> { - sb.append( - " final AssetGenImage $fieldName = const AssetGenImage('$fullPath');\n" - ) - } + when (child.type) { + MediaType.IMAGE -> { + sb.append( + " final AssetGenImage $fieldName = const AssetGenImage('$fullPath');\n" + ) + } - MediaType.SVG -> { - sb.append( - " final SvgGenImage $fieldName = const SvgGenImage('$fullPath');\n" - ) - } + MediaType.SVG -> { + sb.append( + " final SvgGenImage $fieldName = const SvgGenImage('$fullPath');\n" + ) + } - MediaType.LOTTIE -> { - sb.append( - " final LottieGenImage $fieldName = const LottieGenImage('$fullPath');\n" - ) - } + MediaType.LOTTIE -> { + sb.append( + " final LottieGenImage $fieldName = const LottieGenImage('$fullPath');\n" + ) + } - MediaType.RIVE -> { - sb.append( - " final RiveGenImage $fieldName = const RiveGenImage('$fullPath');\n" - ) - } + MediaType.RIVE -> { + sb.append( + " final RiveGenImage $fieldName = const RiveGenImage('$fullPath');\n" + ) + } - else -> { - sb.append(" final String $fieldName = '$fullPath';\n") - } + else -> { + sb.append(" final String $fieldName = '$fullPath';\n") } } } @@ -1014,24 +952,32 @@ $packageDecl } // 生成安全的变量名 (驼峰命名,处理关键词冲突和特殊字符) - private fun getSafeName(name: String, allowLeadingDigit: Boolean = false): String { - val filtered = sanitizeName(name) - val nameStyle = FileHelperNew.getNameStyle(config) - var newName = - if (nameStyle == Constants.NAME_STYLE_SNAKE) { - filtered - .replace(Regex("[-.]"), "_") - .replace(Regex("_+"), "_") - .trim('_') - .lowercase() - } else { - filtered.toLowCamelCase(Regex("[-_.]")) - } + private fun getSafeName(name: String): String { + // 1. Remove Diacritics + val normalized = Normalizer.normalize(name, Normalizer.Form.NFD) + val withoutDiacritics = normalized.replace(Regex("\\p{InCombiningDiacriticalMarks}+"), "") + + // 2. Filter special characters (existing logic) + val filtered = + withoutDiacritics + .replace(Regex("[\\s()\\[\\]{}'\"`!@#$%^&*+=|\\\\:;,<>?/]"), "_") + .replace(Regex("_+"), "_") // 将连续的下划线替换为单个 + .replace("-", "_")// 将连字符替换为下划线 + .trim('_') // 移除首尾的下划线 + + // 转换为驼峰命名 + var newName = if (FileHelperNew.isNamedUseCamelCase(config)) { + filtered.toLowCamelCase(Regex("[-_.]")) + } else { + filtered + } - if (!allowLeadingDigit && newName.isNotEmpty() && newName[0].isDigit()) { + // 处理以数字开头的情况 (Dart变量名不能以数字开头) + if (newName.isNotEmpty() && newName[0].isDigit()) { newName = "a$newName" } + // 处理关键词冲突 if (isKeyWord(newName)) { newName = "${newName}_" } @@ -1041,96 +987,12 @@ $packageDecl /** 生成目录对应的类名 策略: 使用节点名称转换为驼峰命名 + "Gen" 后缀,并在前面加上 "$" 符号和根类名 例如: image 目录 -> $AssetsImageGen */ private fun getGenClassName(node: AssetNode): String { - return generatedDirectoryClassNames.getOrPut(node.path) { - val pathSegments = - node.path - .split("/") - .filter { it.isNotEmpty() } - .let { segments -> - if (shouldFlattenPathForClassName(segments)) segments.drop(1) else segments - } - - val baseClassName = - buildString { - append("$") - append(FileHelperNew.getGeneratedClassName(config)) - append("Gen") - pathSegments.forEach { segment -> append(toPascalCaseSegment(segment)) } - } - - uniqueDirectoryClassName(baseClassName) - } - } - - private fun shouldFlattenPathForClassName(pathSegments: List): Boolean { - return rootNode.children.size == 1 && - rootNode.children[0].type == MediaType.DIRECTORY && - rootNode.children[0].name == "assets" && - pathSegments.firstOrNull() == "assets" - } - - private fun toPascalCaseSegment(segment: String): String { - val sanitized = - sanitizeName(segment) - .replace(Regex("[-.]"), "_") - .split("_") - .filter { it.isNotEmpty() } - .joinToString("") { part -> - part.lowercase(Locale.getDefault()).replaceFirstChar { it.uppercase() } - } - - return sanitized.ifEmpty { "Unnamed" }.let { - if (it.first().isDigit()) "A$it" else it - } - } - - private fun uniqueDirectoryClassName(baseClassName: String): String { - var className = baseClassName - var suffix = 2 - while (!usedDirectoryClassNames.add(className)) { - className = "${baseClassName}${suffix}" - suffix++ - } - return className - } - - private fun buildLeafPath(fullPath: String): String { - if (!FileHelperNew.isPackageParameterEnabled(config)) { - return fullPath - } - - val packageName = config.map["name"] as? String ?: return fullPath - return "packages/$packageName/$fullPath" - } - - private fun sanitizeName(name: String): String { - val normalized = Normalizer.normalize(name, Normalizer.Form.NFD) - val withoutDiacritics = - normalized.replace(Regex("\\p{InCombiningDiacriticalMarks}+"), "") - - return withoutDiacritics - .replace(Regex("[\\s()\\[\\]{}'\"`!@#$%^&*+=|\\\\:;,<>?/]"), "_") - .replace(Regex("_+"), "_") - .trim('_') - } - - private fun combineIdentifier(parentName: String, childName: String, nameStyle: String): String { - if (parentName.isEmpty()) return childName - if (childName.isEmpty()) return parentName - - return if (nameStyle == Constants.NAME_STYLE_SNAKE) { - "${parentName}_${childName}" - } else { - parentName + childName.replaceFirstChar { it.uppercase() } - } - } - - private fun appendExtensionSuffix(baseName: String, extName: String): String { - return if (FileHelperNew.getNameStyle(config) == Constants.NAME_STYLE_SNAKE) { - "${baseName}_${extName}" - } else { - baseName + extName.replaceFirstChar { it.uppercase() } - } + // 使用节点名称而不是完整路径 + val nodeName = node.name + val safeName = nodeName.replace("-", "_").replace(".", "_") + val camelCaseName = + safeName.split("_").joinToString("") { it.replaceFirstChar { c -> c.uppercase() } } + return "$" + FileHelperNew.getGeneratedClassName(config) + camelCaseName + "Gen" } private fun isKeyWord(name: String): Boolean { diff --git a/src/main/java/com/crzsc/plugin/utils/FileHelperNew.kt b/src/main/java/com/crzsc/plugin/utils/FileHelperNew.kt index 0412c30..9c3df38 100644 --- a/src/main/java/com/crzsc/plugin/utils/FileHelperNew.kt +++ b/src/main/java/com/crzsc/plugin/utils/FileHelperNew.kt @@ -282,6 +282,11 @@ object FileHelperNew { return readSetting(config, Constants.KEY_NAMED_WITH_PARENT) as Boolean? ?: true } + /** 是否使用驼峰命名法 (默认 true) */ + fun isNamedUseCamelCase(config: ModulePubSpecConfig): Boolean { + return readSetting(config, Constants.KEY_NAMED_USE_CAMEL_CASE) as Boolean? ?: true + } + /** 读取命名风格配置 */ fun getNameStyle(config: ModulePubSpecConfig): String { return when (readSetting(config, Constants.KEY_NAME_STYLE) as? String) {