Skip to content

Commit b29c8cc

Browse files
authored
Issue 6 percent escaping (#7)
* Issue-6: Fixed capture of %% immediately after an argument match when annotated/spanned * Updated the version to 1.0.1
1 parent fb493e9 commit b29c8cc

4 files changed

Lines changed: 30 additions & 8 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ repositories {
2525
2626
dependencies {
2727
//...
28-
implementation 'com.devbrackets.android:annores:1.0.0'
28+
implementation 'com.devbrackets.android:annores:1.0.1'
2929
}
3030
```
3131

@@ -51,7 +51,7 @@ fun AnAnnotatedComposable() {
5151

5252
# License
5353

54-
Copyright 2023 Brian Wernick
54+
Copyright 2024 Brian Wernick
5555

5656
Licensed under the Apache License, Version 2.0 (the "License");
5757
you may not use this file except in compliance with the License.

library/src/main/kotlin/com/devbrackets/android/annores/text/span/SpannedFormatter.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,16 @@ object SpannedFormatter {
7373
formatArgs: Array<out Any>
7474
): SpannableStringBuilder {
7575
val state = FormatState(locale, formatArgs)
76-
77-
FORMAT_REGEX.findAll(this, 0).forEach { match ->
78-
replace(state, match)
79-
}
76+
var startIndex = 0
77+
78+
// Manually iterate over the matches for 2 reasons:
79+
// 1. The `FORMAT_REGEX.findAll()` wasn't correctly capturing all cases (e.g. %1$d%% only caught the first match)
80+
// 2. Use the `startIndex` so that a replacement of another format doesn't work (e.g. %d -> %d) since that would cause a cycle
81+
do {
82+
val match = FORMAT_REGEX.find(this, startIndex)?.also {
83+
startIndex = replace(state, it)
84+
}
85+
} while (match != null)
8086

8187
return this
8288
}
@@ -87,12 +93,14 @@ object SpannedFormatter {
8793
*
8894
* @param state The [FormatState] to use for replacing the format placeholder from [match]
8995
* @param match The [MatchResult] holding a match from the [FORMAT_REGEX] which should be replaced
96+
*
97+
* @return The end index of the replacement
9098
*/
9199
@Suppress("MoveVariableDeclarationIntoWhen")
92100
private fun SpannableStringBuilder.replace(
93101
state: FormatState,
94102
match: MatchResult
95-
) {
103+
): Int {
96104
val argIndex = match.groupValues[1]
97105
val format = match.groupValues[2]
98106
val conversion = match.groupValues[3]
@@ -104,6 +112,7 @@ object SpannedFormatter {
104112
}
105113

106114
replace(match.range.first, match.range.last + 1, replacementValue)
115+
return match.range.first + replacementValue.length
107116
}
108117

109118
/**

library/src/test/kotlin/com/devbrackets/android/annores/text/span/SpannedFormatterTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ class SpannedFormatterTest {
4545
Assert.assertEquals("A string %", actual.toString())
4646
}
4747

48+
@Test
49+
fun formatPercentReplacement() {
50+
// Given
51+
val locale = Locale("en", "us")
52+
val spanned = SpannedString("%1\$d%%")
53+
54+
// When
55+
val actual = SpannedFormatter.format(locale, spanned, 25)
56+
57+
// Then
58+
Assert.assertEquals("25%", actual.toString())
59+
}
60+
4861
@Test
4962
fun formatNewLine() {
5063
// Given

libraryInfo.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ GROUP_ID = com.devbrackets.android
33

44
VERSION_MAJOR = 1
55
VERSION_MINOR = 0
6-
VERSION_PATCH = 0
6+
VERSION_PATCH = 1

0 commit comments

Comments
 (0)