Browse Source

[gradle] Add xml values converter.

pull/4559/head
Konstantin Tskhovrebov 2 months ago
parent
commit
d54a4ce588
  1. 63
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt
  2. 11
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt
  3. 18
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt
  4. 136
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/XmlValuesConverterTask.kt

63
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GenerateResClassTask.kt

@ -1,12 +1,11 @@
package org.jetbrains.compose.resources
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import java.io.File
import java.io.RandomAccessFile
import java.nio.file.Path
import javax.xml.parsers.DocumentBuilderFactory
import kotlin.io.path.relativeTo
/**
@ -30,13 +29,17 @@ internal abstract class GenerateResClassTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resDir: Property<File>
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val convertedXmlValuesDir: Property<File>
@get:OutputDirectory
abstract val codeDir: DirectoryProperty
abstract val codeDir: Property<File>
@TaskAction
fun generate() {
try {
val kotlinDir = codeDir.get().asFile
val kotlinDir = codeDir.get()
logger.info("Clean directory $kotlinDir")
kotlinDir.deleteRecursively()
kotlinDir.mkdirs()
@ -100,35 +103,47 @@ internal abstract class GenerateResClassTask : DefaultTask() {
return null
}
if (typeString == "values" && file.name.equals("strings.xml", true)) {
return getStringResources(file).mapNotNull { (typeName, strId) ->
val type = when(typeName) {
"string", "string-array" -> ResourceType.STRING
"plurals" -> ResourceType.PLURAL_STRING
else -> return@mapNotNull null
}
ResourceItem(type, qualifiers, strId.asUnderscoredIdentifier(), path)
}
if (typeString == "values" && file.extension.equals("xml", true)) {
val converted = convertedXmlValuesDir.get()
.resolve(file.parentFile.name)
.resolve(file.nameWithoutExtension + ".${XmlValuesConverterTask.CONVERTED_RESOURCE_EXT}")
return getValueResourceItems(converted, qualifiers, path.parent.resolve(converted.name))
}
val type = ResourceType.fromString(typeString)
val type = ResourceType.fromString(typeString) ?: error("Unknown resource type: '$typeString'.")
return listOf(ResourceItem(type, qualifiers, file.nameWithoutExtension.asUnderscoredIdentifier(), path))
}
//type -> id
private val stringTypeNames = listOf("string", "string-array", "plurals")
private fun getStringResources(stringsXml: File): List<Pair<String, String>> {
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(stringsXml)
val items = doc.getElementsByTagName("resources").item(0).childNodes
return List(items.length) { items.item(it) }
.filter { it.nodeName in stringTypeNames }
.map { it.nodeName to it.attributes.getNamedItem("name").nodeValue }
private fun getValueResourceItems(dataFile: File, qualifiers: List<String>, path: Path) : List<ResourceItem> {
val result = mutableListOf<ResourceItem>()
RandomAccessFile(dataFile, "r").use { f ->
var offset: Long = 0
var line: String? = f.readLine()
while (line != null) {
val size = line.encodeToByteArray().size.toLong()
result.add(getValueResourceItem(line, offset, size, qualifiers, path))
offset += size + 1 // "+1" for newline character
line = f.readLine()
}
}
return result
}
private fun File.listNotHiddenFiles(): List<File> =
listFiles()?.filter { !it.isHidden }.orEmpty()
private fun getValueResourceItem(
recordString: String,
offset: Long,
size: Long,
qualifiers: List<String>,
path: Path
) : ResourceItem {
val record = ValueResourceRecord.createFromString(recordString)
return ResourceItem(record.type, qualifiers, record.key.asUnderscoredIdentifier(), path.resolve("$offset-$size"))
}
}
internal fun File.listNotHiddenFiles(): List<File> =
listFiles()?.filter { !it.isHidden }.orEmpty()
internal fun String.asUnderscoredIdentifier(): String =
replace('-', '_')
.let { if (it.isNotEmpty() && it.first().isDigit()) "_$it" else it }

11
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesGenerator.kt

@ -300,7 +300,7 @@ private fun Project.configureResourceGenerator(
) {
logger.info("Configure accessors for '${commonSourceSet.name}'")
fun buildDir(path: String) = layout.dir(layout.buildDirectory.map { File(it.asFile, path) })
fun buildDir(path: String) = layout.buildDirectory.asFile.map { it.resolve(path) }
//lazy check a dependency on the Resources library
val shouldGenerateResClass = generateResClassMode.map { mode ->
@ -329,6 +329,14 @@ private fun Project.configureResourceGenerator(
}
}
val xmlValuesConverterTask = tasks.register(
"convertXmlValues",
XmlValuesConverterTask::class.java
) { task ->
task.originalResourcesDir.set(commonComposeResourcesDir)
task.outputDir.set(buildDir("$RES_GEN_DIR/convertedXmlValues"))
}
val genTask = tasks.register(
"generateComposeResClass",
GenerateResClassTask::class.java
@ -338,6 +346,7 @@ private fun Project.configureResourceGenerator(
task.makeResClassPublic.set(publicResClass)
task.resDir.set(commonComposeResourcesDir)
task.codeDir.set(buildDir("$RES_GEN_DIR/kotlin"))
task.convertedXmlValuesDir.set(xmlValuesConverterTask.flatMap { it.outputDir })
if (generateModulePath) {
task.moduleDir.set(resourcePackage.asModuleDir())

18
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/ResourcesSpec.kt

@ -6,12 +6,12 @@ import java.nio.file.Path
import java.util.*
import kotlin.io.path.invariantSeparatorsPathString
internal enum class ResourceType(val typeName: String) {
DRAWABLE("drawable"),
STRING("string"),
STRING_ARRAY("string-array"),
PLURAL_STRING("plurals"),
FONT("font");
internal enum class ResourceType(val typeName: String, val accessorName: String) {
DRAWABLE("drawable", "drawable"),
STRING("string", "string"),
STRING_ARRAY("string-array", "array"),
PLURAL_STRING("plurals", "plurals"),
FONT("font", "font");
override fun toString(): String = typeName
@ -159,7 +159,7 @@ internal fun getResFileSpecs(
.build()
)
ResourceType.values().forEach { type ->
resObject.addType(TypeSpec.objectBuilder(type.typeName).build())
resObject.addType(TypeSpec.objectBuilder(type.accessorName).build())
}
}.build())
}.build()
@ -194,7 +194,7 @@ private fun getChunkFileSpec(
resModifier: KModifier,
idToResources: Map<String, List<ResourceItem>>
): FileSpec {
val chunkClassName = type.typeName.uppercaseFirstChar() + index
val chunkClassName = type.accessorName.uppercaseFirstChar() + index
return FileSpec.builder(packageName, chunkClassName).also { chunkFile ->
chunkFile.addAnnotation(
AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
@ -216,7 +216,7 @@ private fun getChunkFileSpec(
idToResources.forEach { (resName, items) ->
val accessor = PropertySpec.builder(resName, type.getClassName(), resModifier)
.receiver(ClassName(packageName, "Res", type.typeName))
.receiver(ClassName(packageName, "Res", type.accessorName))
.addAnnotation(experimentalAnnotation)
.getter(FunSpec.getterBuilder().addStatement("return $chunkClassName.$resName").build())
.build()

136
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/XmlValuesConverterTask.kt

@ -0,0 +1,136 @@
package org.jetbrains.compose.resources
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.w3c.dom.Node
import java.io.File
import java.util.*
import javax.xml.parsers.DocumentBuilderFactory
internal data class ValueResourceRecord(
val type: ResourceType,
val key: String,
val content: String
) {
fun getAsString(): String {
return listOf(type.typeName, key, content).joinToString("#")
}
companion object {
fun createFromString(string: String): ValueResourceRecord {
val parts = string.split("#")
return ValueResourceRecord(
ResourceType.fromString(parts[0])!!,
parts[1],
parts[2]
)
}
}
}
internal abstract class XmlValuesConverterTask : DefaultTask() {
companion object {
const val CONVERTED_RESOURCE_EXT = "cvr" //Compose Value Resource
}
@get:InputFiles
abstract val originalResourcesDir: Property<File>
@get:OutputDirectory
abstract val outputDir: Property<File>
@TaskAction
fun run() {
val dir = outputDir.get()
dir.deleteRecursively()
originalResourcesDir.get().listNotHiddenFiles().forEach { dir ->
if (dir.isDirectory && dir.name.startsWith("values")) {
dir.listNotHiddenFiles().forEach { f ->
if (f.extension.equals("xml", true)) {
val output = dir
.resolve(f.parentFile.name)
.resolve(f.nameWithoutExtension + ".$CONVERTED_RESOURCE_EXT")
output.parentFile.mkdirs()
convert(f, output)
}
}
}
}
}
private fun convert(original: File, converted: File) {
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(original)
val items = doc.getElementsByTagName("resources").item(0).childNodes
val records = List(items.length) { items.item(it) }.mapNotNull { getItemRecord(it)?.getAsString() }
converted.writeText(records.sorted().joinToString("\n"))
}
private fun getItemRecord(node: Node): ValueResourceRecord? {
val type = ResourceType.fromString(node.nodeName) ?: return null
val key = node.attributes.getNamedItem("name").nodeValue
val value: String
when (type) {
ResourceType.STRING -> {
val content = handleSpecialCharacters(node.textContent)
value = content.asBase64()
}
ResourceType.STRING_ARRAY -> {
val children = node.childNodes
value = List(children.length) { children.item(it) }
.filter { it.nodeName == "item" }
.joinToString(",") { child ->
val content = handleSpecialCharacters(child.textContent)
content.asBase64()
}
}
ResourceType.PLURAL_STRING -> {
val children = node.childNodes
value = List(children.length) { children.item(it) }
.filter { it.nodeName == "item" }
.joinToString(",") { child ->
val content = handleSpecialCharacters(child.textContent)
val quantity = child.attributes.getNamedItem("quantity").nodeValue
quantity.uppercase() + ":" + content.asBase64()
}
}
else -> error("Unknown string resource type: '$type'")
}
return ValueResourceRecord(type, key, value)
}
private fun String.asBase64() =
Base64.getEncoder().encode(this.encodeToByteArray()).decodeToString()
//https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes
/**
* Replaces
*
* '\n' -> new line
*
* '\t' -> tab
*
* '\uXXXX' -> unicode symbol
*
* '\\' -> '\'
*
* @param string The input string to handle.
* @return The string with special characters replaced according to the logic.
*/
private fun handleSpecialCharacters(string: String): String {
val unicodeNewLineTabRegex = Regex("""\\u[a-fA-F\d]{4}|\\n|\\t""")
val doubleSlashRegex = Regex("""\\\\""")
val doubleSlashIndexes = doubleSlashRegex.findAll(string).map { it.range.first }
val handledString = unicodeNewLineTabRegex.replace(string) { matchResult ->
if (doubleSlashIndexes.contains(matchResult.range.first - 1)) matchResult.value
else when (matchResult.value) {
"\\n" -> "\n"
"\\t" -> "\t"
else -> matchResult.value.substring(2).toInt(16).toChar().toString()
}
}.replace("""\\""", """\""")
return handledString
}
}
Loading…
Cancel
Save