You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
269 lines
11 KiB
269 lines
11 KiB
package org.jetbrains.compose.resources |
|
|
|
import com.squareup.kotlinpoet.* |
|
import org.jetbrains.compose.internal.utils.uppercaseFirstChar |
|
import java.nio.file.Path |
|
import java.util.* |
|
import kotlin.io.path.invariantSeparatorsPathString |
|
|
|
internal enum class ResourceType(val typeName: String) { |
|
DRAWABLE("drawable"), |
|
STRING("string"), |
|
PLURAL_STRING("plurals"), |
|
FONT("font"); |
|
|
|
override fun toString(): String = typeName |
|
|
|
companion object { |
|
fun fromString(str: String): ResourceType = |
|
ResourceType.values() |
|
.firstOrNull { it.typeName.equals(str, true) } |
|
?: error("Unknown resource type: '$str'.") |
|
} |
|
} |
|
|
|
internal data class ResourceItem( |
|
val type: ResourceType, |
|
val qualifiers: List<String>, |
|
val name: String, |
|
val path: Path |
|
) |
|
|
|
private fun ResourceType.getClassName(): ClassName = when (this) { |
|
ResourceType.DRAWABLE -> ClassName("org.jetbrains.compose.resources", "DrawableResource") |
|
ResourceType.STRING -> ClassName("org.jetbrains.compose.resources", "StringResource") |
|
ResourceType.PLURAL_STRING -> ClassName("org.jetbrains.compose.resources", "PluralStringResource") |
|
ResourceType.FONT -> ClassName("org.jetbrains.compose.resources", "FontResource") |
|
} |
|
|
|
private val resourceItemClass = ClassName("org.jetbrains.compose.resources", "ResourceItem") |
|
private val experimentalAnnotation = AnnotationSpec.builder( |
|
ClassName("org.jetbrains.compose.resources", "ExperimentalResourceApi") |
|
).build() |
|
|
|
private fun CodeBlock.Builder.addQualifiers(resourceItem: ResourceItem): CodeBlock.Builder { |
|
val languageQualifier = ClassName("org.jetbrains.compose.resources", "LanguageQualifier") |
|
val regionQualifier = ClassName("org.jetbrains.compose.resources", "RegionQualifier") |
|
val themeQualifier = ClassName("org.jetbrains.compose.resources", "ThemeQualifier") |
|
val densityQualifier = ClassName("org.jetbrains.compose.resources", "DensityQualifier") |
|
|
|
val languageRegex = Regex("[a-z]{2,3}") |
|
val regionRegex = Regex("r[A-Z]{2}") |
|
|
|
val qualifiersMap = mutableMapOf<ClassName, String>() |
|
|
|
fun saveQualifier(className: ClassName, qualifier: String) { |
|
qualifiersMap[className]?.let { |
|
error("${resourceItem.path} contains repetitive qualifiers: '$it' and '$qualifier'.") |
|
} |
|
qualifiersMap[className] = qualifier |
|
} |
|
|
|
resourceItem.qualifiers.forEach { q -> |
|
when (q) { |
|
"light", |
|
"dark" -> { |
|
saveQualifier(themeQualifier, q) |
|
} |
|
|
|
"mdpi", |
|
"hdpi", |
|
"xhdpi", |
|
"xxhdpi", |
|
"xxxhdpi", |
|
"ldpi" -> { |
|
saveQualifier(densityQualifier, q) |
|
} |
|
|
|
else -> when { |
|
q.matches(languageRegex) -> { |
|
saveQualifier(languageQualifier, q) |
|
} |
|
|
|
q.matches(regionRegex) -> { |
|
saveQualifier(regionQualifier, q) |
|
} |
|
|
|
else -> error("${resourceItem.path} contains unknown qualifier: '$q'.") |
|
} |
|
} |
|
} |
|
qualifiersMap[themeQualifier]?.let { q -> add("%T.${q.uppercase()}, ", themeQualifier) } |
|
qualifiersMap[densityQualifier]?.let { q -> add("%T.${q.uppercase()}, ", densityQualifier) } |
|
qualifiersMap[languageQualifier]?.let { q -> add("%T(\"$q\"), ", languageQualifier) } |
|
qualifiersMap[regionQualifier]?.let { q -> |
|
val lang = qualifiersMap[languageQualifier] |
|
if (lang == null) { |
|
error("Region qualifier must be used only with language.\nFile: ${resourceItem.path}") |
|
} |
|
val langAndRegion = "$lang-$q" |
|
if (!resourceItem.path.toString().contains("-$langAndRegion")) { |
|
error("Region qualifier must be declared after language: '$langAndRegion'.\nFile: ${resourceItem.path}") |
|
} |
|
add("%T(\"${q.takeLast(2)}\"), ", regionQualifier) |
|
} |
|
|
|
return this |
|
} |
|
|
|
// We need to divide accessors by different files because |
|
// |
|
// if all accessors are generated in a single object |
|
// then a build may fail with: org.jetbrains.org.objectweb.asm.MethodTooLargeException: Method too large: Res$drawable.<clinit> ()V |
|
// e.g. https://github.com/JetBrains/compose-multiplatform/issues/4285 |
|
// |
|
// if accessor initializers are extracted from the single object but located in the same file |
|
// then a build may fail with: org.jetbrains.org.objectweb.asm.ClassTooLargeException: Class too large: Res$drawable |
|
private const val ITEMS_PER_FILE_LIMIT = 500 |
|
internal fun getResFileSpecs( |
|
//type -> id -> items |
|
resources: Map<ResourceType, Map<String, List<ResourceItem>>>, |
|
packageName: String, |
|
moduleDir: String, |
|
isPublic: Boolean |
|
): List<FileSpec> { |
|
val resModifier = if (isPublic) KModifier.PUBLIC else KModifier.INTERNAL |
|
val files = mutableListOf<FileSpec>() |
|
val resClass = FileSpec.builder(packageName, "Res").also { file -> |
|
file.addAnnotation( |
|
AnnotationSpec.builder(ClassName("kotlin", "OptIn")) |
|
.addMember("org.jetbrains.compose.resources.InternalResourceApi::class") |
|
.addMember("org.jetbrains.compose.resources.ExperimentalResourceApi::class") |
|
.build() |
|
) |
|
file.addType(TypeSpec.objectBuilder("Res").also { resObject -> |
|
resObject.addModifiers(resModifier) |
|
resObject.addAnnotation(experimentalAnnotation) |
|
|
|
//readFileBytes |
|
val readResourceBytes = MemberName("org.jetbrains.compose.resources", "readResourceBytes") |
|
resObject.addFunction( |
|
FunSpec.builder("readBytes") |
|
.addKdoc( |
|
""" |
|
Reads the content of the resource file at the specified path and returns it as a byte array. |
|
|
|
Example: `val bytes = Res.readBytes("files/key.bin")` |
|
|
|
@param path The path of the file to read in the compose resource's directory. |
|
@return The content of the file as a byte array. |
|
""".trimIndent() |
|
) |
|
.addParameter("path", String::class) |
|
.addModifiers(KModifier.SUSPEND) |
|
.returns(ByteArray::class) |
|
.addStatement("""return %M("$moduleDir" + path)""", readResourceBytes) |
|
.build() |
|
) |
|
ResourceType.values().forEach { type -> |
|
resObject.addType(TypeSpec.objectBuilder(type.typeName).build()) |
|
} |
|
}.build()) |
|
}.build() |
|
files.add(resClass) |
|
|
|
//we need to sort it to generate the same code on different platforms |
|
sortResources(resources).forEach { (type, idToResources) -> |
|
val chunks = idToResources.keys.chunked(ITEMS_PER_FILE_LIMIT) |
|
|
|
chunks.forEachIndexed { index, ids -> |
|
files.add( |
|
getChunkFileSpec( |
|
type, |
|
index, |
|
packageName, |
|
moduleDir, |
|
resModifier, |
|
idToResources.subMap(ids.first(), true, ids.last(), true) |
|
) |
|
) |
|
} |
|
} |
|
|
|
return files |
|
} |
|
|
|
private fun getChunkFileSpec( |
|
type: ResourceType, |
|
index: Int, |
|
packageName: String, |
|
moduleDir: String, |
|
resModifier: KModifier, |
|
idToResources: Map<String, List<ResourceItem>> |
|
): FileSpec { |
|
val chunkClassName = type.typeName.uppercaseFirstChar() + index |
|
return FileSpec.builder(packageName, chunkClassName).also { chunkFile -> |
|
chunkFile.addAnnotation( |
|
AnnotationSpec.builder(ClassName("kotlin", "OptIn")) |
|
.addMember("org.jetbrains.compose.resources.InternalResourceApi::class") |
|
.build() |
|
) |
|
|
|
val objectSpec = TypeSpec.objectBuilder(chunkClassName).also { typeObject -> |
|
typeObject.addModifiers(KModifier.PRIVATE) |
|
typeObject.addAnnotation(experimentalAnnotation) |
|
val properties = idToResources.keys.map { resName -> |
|
PropertySpec.builder(resName, type.getClassName()) |
|
.delegate("\nlazy·{ init_$resName() }") |
|
.build() |
|
} |
|
typeObject.addProperties(properties) |
|
}.build() |
|
chunkFile.addType(objectSpec) |
|
|
|
idToResources.forEach { (resName, items) -> |
|
val accessor = PropertySpec.builder(resName, type.getClassName(), resModifier) |
|
.receiver(ClassName(packageName, "Res", type.typeName)) |
|
.addAnnotation(experimentalAnnotation) |
|
.getter(FunSpec.getterBuilder().addStatement("return $chunkClassName.$resName").build()) |
|
.build() |
|
chunkFile.addProperty(accessor) |
|
|
|
val initializer = FunSpec.builder("init_$resName") |
|
.addModifiers(KModifier.PRIVATE) |
|
.addAnnotation(experimentalAnnotation) |
|
.returns(type.getClassName()) |
|
.addStatement( |
|
CodeBlock.builder() |
|
.add("return %T(\n", type.getClassName()).withIndent { |
|
add("\"${type}:${resName}\",") |
|
if (type == ResourceType.STRING || type == ResourceType.PLURAL_STRING) add(" \"$resName\",") |
|
withIndent { |
|
add("\nsetOf(\n").withIndent { |
|
items.forEach { item -> |
|
add("%T(", resourceItemClass) |
|
add("setOf(").addQualifiers(item).add("), ") |
|
//file separator should be '/' on all platforms |
|
add("\"$moduleDir${item.path.invariantSeparatorsPathString}\"") |
|
add("),\n") |
|
} |
|
} |
|
add(")\n") |
|
} |
|
} |
|
.add(")") |
|
.build().toString() |
|
) |
|
.build() |
|
chunkFile.addFunction(initializer) |
|
} |
|
}.build() |
|
} |
|
|
|
private fun sortResources( |
|
resources: Map<ResourceType, Map<String, List<ResourceItem>>> |
|
): TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>> { |
|
val result = TreeMap<ResourceType, TreeMap<String, List<ResourceItem>>>() |
|
resources |
|
.entries |
|
.forEach { (type, items) -> |
|
val typeResult = TreeMap<String, List<ResourceItem>>() |
|
items |
|
.entries |
|
.forEach { (name, resItems) -> |
|
typeResult[name] = resItems.sortedBy { it.path } |
|
} |
|
result[type] = typeResult |
|
} |
|
return result |
|
} |