Fix tag highlighting in editor

This commit is contained in:
kyori19 2020-05-04 15:18:04 +09:00
parent ee345d58f9
commit 8be0989a73
2 changed files with 38 additions and 12 deletions

View File

@ -12,7 +12,7 @@ import kotlin.math.max
* @see <a href="https://github.com/tootsuite/mastodon/blob/master/app/models/tag.rb"> * @see <a href="https://github.com/tootsuite/mastodon/blob/master/app/models/tag.rb">
* Tag#HASHTAG_RE</a>. * Tag#HASHTAG_RE</a>.
*/ */
private const val TAG_REGEX = "(?:^|[^/)\\w])#([\\w_]*[\\p{Alpha}_][\\w_]*)" private const val TAG_REGEX = "(?:^|[^/)A-Za-z0-9_])#([\\w_]*[\\p{Alpha}_][\\w_]*)"
/** /**
* @see <a href="https://github.com/tootsuite/mastodon/blob/master/app/models/account.rb"> * @see <a href="https://github.com/tootsuite/mastodon/blob/master/app/models/account.rb">
@ -30,10 +30,10 @@ private val STRICT_WEB_URL_PATTERN = Pattern.compile("(((?:(?i:http|https|rtsp):
private val spanClasses = listOf(ForegroundColorSpan::class.java, URLSpan::class.java) private val spanClasses = listOf(ForegroundColorSpan::class.java, URLSpan::class.java)
private val finders = mapOf( private val finders = mapOf(
FoundMatchType.HTTP_URL to PatternFinder(':', HTTP_URL_REGEX, 5), FoundMatchType.HTTP_URL to PatternFinder(':', HTTP_URL_REGEX, 5, Character::isWhitespace),
FoundMatchType.HTTPS_URL to PatternFinder(':', HTTPS_URL_REGEX, 6), FoundMatchType.HTTPS_URL to PatternFinder(':', HTTPS_URL_REGEX, 6, Character::isWhitespace),
FoundMatchType.TAG to PatternFinder('#', TAG_REGEX, 1), FoundMatchType.TAG to PatternFinder('#', TAG_REGEX, 1, ::isValidForTagPrefix),
FoundMatchType.MENTION to PatternFinder('@', MENTION_REGEX, 1) FoundMatchType.MENTION to PatternFinder('@', MENTION_REGEX, 1, Character::isWhitespace) // TODO: We also need a proper validator for mentions
) )
private enum class FoundMatchType { private enum class FoundMatchType {
@ -49,7 +49,8 @@ private class FindCharsResult {
var end: Int = -1 var end: Int = -1
} }
private class PatternFinder(val searchCharacter: Char, regex: String, val searchPrefixWidth: Int) { private class PatternFinder(val searchCharacter: Char, regex: String, val searchPrefixWidth: Int,
val prefixValidator: (Int) -> Boolean) {
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE) val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
} }
@ -67,7 +68,7 @@ private fun findPattern(string: String, fromIndex: Int): FindCharsResult {
val finder = finders[matchType] val finder = finders[matchType]
if (finder!!.searchCharacter == c if (finder!!.searchCharacter == c
&& ((i - fromIndex) < finder.searchPrefixWidth || && ((i - fromIndex) < finder.searchPrefixWidth ||
Character.isWhitespace(string.codePointAt(i - finder.searchPrefixWidth)))) { finder.prefixValidator(string.codePointAt(i - finder.searchPrefixWidth)))) {
result.matchType = matchType result.matchType = matchType
result.start = max(0, i - finder.searchPrefixWidth) result.start = max(0, i - finder.searchPrefixWidth)
findEndOfPattern(string, result, finder.pattern) findEndOfPattern(string, result, finder.pattern)
@ -87,10 +88,22 @@ private fun findEndOfPattern(string: String, result: FindCharsResult, pattern: P
// Once we have API level 26+, we can use named captures... // Once we have API level 26+, we can use named captures...
val end = matcher.end() val end = matcher.end()
result.start = matcher.start() result.start = matcher.start()
if (Character.isWhitespace(string.codePointAt(result.start))) { when (result.matchType) {
++result.start FoundMatchType.TAG -> {
if (isValidForTagPrefix(string.codePointAt(result.start))) {
if (string[result.start] != '#' ||
(string[result.start] == '#' && string[result.start + 1] == '#')) {
++result.start
}
}
}
else -> {
if (Character.isWhitespace(string.codePointAt(result.start))) {
++result.start
}
}
} }
when(result.matchType) { when (result.matchType) {
FoundMatchType.HTTP_URL, FoundMatchType.HTTPS_URL -> { FoundMatchType.HTTP_URL, FoundMatchType.HTTPS_URL -> {
// Preliminary url patterns are fast/permissive, now we'll do full validation // Preliminary url patterns are fast/permissive, now we'll do full validation
if (STRICT_WEB_URL_PATTERN.matcher(string.substring(result.start, end)).matches()) { if (STRICT_WEB_URL_PATTERN.matcher(string.substring(result.start, end)).matches()) {
@ -133,3 +146,16 @@ fun highlightSpans(text: Spannable, colour: Int) {
} }
} }
} }
private fun isWordCharacters(codePoint: Int): Boolean {
return (codePoint in 0x30..0x39) || // [0-9]
(codePoint in 0x41..0x5a) || // [A-Z]
(codePoint == 0x5f) || // _
(codePoint in 0x61..0x7a) // [a-z]
}
private fun isValidForTagPrefix(codePoint: Int): Boolean {
return !(isWordCharacters(codePoint) || // \w
(codePoint == 0x2f) || // /
(codePoint == 0x29)) // )
}

View File

@ -10,11 +10,11 @@ import org.junit.runners.Parameterized
class SpanUtilsTest { class SpanUtilsTest {
@Test @Test
fun matchesMixedSpans() { fun matchesMixedSpans() {
val input = "one #one two: @two three : https://thr.ee/meh?foo=bar&wat=@at#hmm four #four five @five" val input = "one #one two: @two three : https://thr.ee/meh?foo=bar&wat=@at#hmm four #four five @five ろく#six"
val inputSpannable = FakeSpannable(input) val inputSpannable = FakeSpannable(input)
highlightSpans(inputSpannable, 0xffffff) highlightSpans(inputSpannable, 0xffffff)
val spans = inputSpannable.spans val spans = inputSpannable.spans
Assert.assertEquals(5, spans.size) Assert.assertEquals(6, spans.size)
} }
@Test @Test