Browse Source

[resources] Add parsing serialized string resources into custom format

To avoid slow XML parsing
pull/4559/head
Konstantin Tskhovrebov 2 months ago
parent
commit
12e60b736a
  1. 4
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt
  2. 5
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt
  3. 4
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt
  4. 4
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt
  5. 104
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt

4
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/PluralStringResources.kt

@ -57,9 +57,7 @@ private suspend fun loadPluralString(
environment: ResourceEnvironment
): String {
val path = resource.getPathByEnvironment(environment)
val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Plurals
?: error("Quantity string ID=`${resource.key}` is not found!")
val item = getStringItem(path, resourceReader) as StringItem.Plurals
val pluralRuleList = PluralRuleList.getInstance(
environment.language,
environment.region,

5
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt

@ -16,11 +16,16 @@ expect suspend fun readResourceBytes(path: String): ByteArray
internal interface ResourceReader {
suspend fun read(path: String): ByteArray
suspend fun readPart(path: String, offset: Long, size: Long): ByteArray
}
internal val DefaultResourceReader: ResourceReader = object : ResourceReader {
@OptIn(InternalResourceApi::class)
override suspend fun read(path: String): ByteArray = readResourceBytes(path)
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
TODO("Not yet implemented")
}
}
//ResourceReader provider will be overridden for tests

4
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringArrayResources.kt

@ -59,8 +59,6 @@ private suspend fun loadStringArray(
environment: ResourceEnvironment
): List<String> {
val path = resource.getPathByEnvironment(environment)
val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Array
?: error("String array ID=`${resource.key}` is not found!")
val item = getStringItem(path, resourceReader) as StringItem.Array
return item.items
}

4
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt

@ -52,9 +52,7 @@ private suspend fun loadString(
environment: ResourceEnvironment
): String {
val path = resource.getPathByEnvironment(environment)
val keyToValue = getParsedStrings(path, resourceReader)
val item = keyToValue[resource.key] as? StringItem.Value
?: error("String ID=`${resource.key}` is not found!")
val item = getStringItem(path, resourceReader) as StringItem.Value
return item.text
}

104
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResourcesUtils.kt

@ -6,9 +6,10 @@ import kotlinx.coroutines.sync.withLock
import org.jetbrains.compose.resources.plural.PluralCategory
import org.jetbrains.compose.resources.vector.xmldom.Element
import org.jetbrains.compose.resources.vector.xmldom.NodeList
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
private val SimpleStringFormatRegex = Regex("""%(\d)\$[ds]""")
internal fun String.replaceWithArgs(args: List<String>) = SimpleStringFormatRegex.replace(this) { matchResult ->
args[matchResult.groupValues[1].toInt() - 1]
}
@ -20,84 +21,55 @@ internal sealed interface StringItem {
}
private val stringsCacheMutex = Mutex()
private val parsedStringsCache = mutableMapOf<String, Deferred<Map<String, StringItem>>>()
private val stringItemsCache = mutableMapOf<String, Deferred<StringItem>>()
//@TestOnly
internal fun dropStringsCache() {
parsedStringsCache.clear()
internal fun dropStringItemsCache() {
stringItemsCache.clear()
}
internal suspend fun getParsedStrings(
internal suspend fun getStringItem(
path: String,
resourceReader: ResourceReader
): Map<String, StringItem> = coroutineScope {
): StringItem = coroutineScope {
val deferred = stringsCacheMutex.withLock {
parsedStringsCache.getOrPut(path) {
stringItemsCache.getOrPut(path) {
//LAZY - to free the mutex lock as fast as possible
async(start = CoroutineStart.LAZY) {
parseStringXml(path, resourceReader)
val filePath = path.substringBeforeLast('/')
val recordPath = path.substringAfterLast('/')
val (offset, size) = recordPath.split('-').map { it.toLong() }
val record = resourceReader.readPart(filePath, offset, size).decodeToString()
val recordItems = record.split('#')
val recordType = recordItems.first()
val recordData = recordItems.last()
when (recordType) {
"plurals" -> recordData.decodeAsPlural()
"string-array" -> recordData.decodeAsArray()
else -> recordData.decodeAsString()
}
}
}
}
deferred.await()
}
private suspend fun parseStringXml(path: String, resourceReader: ResourceReader): Map<String, StringItem> {
val nodes = resourceReader.read(path).toXmlElement().childNodes
val strings = nodes.getElementsWithName("string").associate { element ->
val rawString = element.textContent.orEmpty()
element.getAttribute("name") to StringItem.Value(handleSpecialCharacters(rawString))
}
val plurals = nodes.getElementsWithName("plurals").associate { pluralElement ->
val items = pluralElement.childNodes.getElementsWithName("item").mapNotNull { element ->
val pluralCategory = PluralCategory.fromString(
element.getAttribute("quantity"),
) ?: return@mapNotNull null
pluralCategory to handleSpecialCharacters(element.textContent.orEmpty())
}
pluralElement.getAttribute("name") to StringItem.Plurals(items.toMap())
}
val arrays = nodes.getElementsWithName("string-array").associate { arrayElement ->
val items = arrayElement.childNodes.getElementsWithName("item").map { element ->
val rawString = element.textContent.orEmpty()
handleSpecialCharacters(rawString)
}
arrayElement.getAttribute("name") to StringItem.Array(items)
}
return strings + plurals + arrays
}
@OptIn(ExperimentalEncodingApi::class)
private fun String.decodeAsString(): StringItem.Value = StringItem.Value(
Base64.decode(this).decodeToString()
)
private fun NodeList.getElementsWithName(name: String): List<Element> =
List(length) { item(it) }
.filterIsInstance<Element>()
.filter { it.localName == name }
@OptIn(ExperimentalEncodingApi::class)
private fun String.decodeAsArray(): StringItem.Array = StringItem.Array(
split(",").map { item ->
Base64.decode(item).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.
*/
internal 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
}
@OptIn(ExperimentalEncodingApi::class)
private fun String.decodeAsPlural(): StringItem.Plurals = StringItem.Plurals(
split(",").associate { item ->
val category = item.substringBefore(':')
val valueBase64 = item.substringAfter(':')
PluralCategory.fromString(category)!! to Base64.decode(valueBase64).decodeToString()
}
)

Loading…
Cancel
Save