This commit is contained in:
Mariotaku Lee 2017-05-26 19:07:45 +08:00
parent 647767e436
commit 039a022593
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
8 changed files with 78 additions and 24 deletions

View File

@ -93,11 +93,12 @@ public class SpanItem implements Parcelable {
SpanItemParcelablePlease.writeToParcel(this, dest, flags); SpanItemParcelablePlease.writeToParcel(this, dest, flags);
} }
@IntDef({SpanType.HIDE, SpanType.LINK, SpanType.ACCT_MENTION}) @IntDef({SpanType.HIDE, SpanType.LINK, SpanType.ACCT_MENTION, SpanType.HASHTAG})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface SpanType { public @interface SpanType {
int HIDE = -1; int HIDE = -1;
int LINK = 0; int LINK = 0;
int ACCT_MENTION = 1; int ACCT_MENTION = 1;
int HASHTAG = 2;
} }
} }

View File

@ -33,6 +33,7 @@ import com.twitter.Regex;
import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.Constants;
import org.mariotaku.twidere.model.UserKey; import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.text.AcctMentionSpan; import org.mariotaku.twidere.text.AcctMentionSpan;
import org.mariotaku.twidere.text.HashtagSpan;
import org.mariotaku.twidere.text.TwidereURLSpan; import org.mariotaku.twidere.text.TwidereURLSpan;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -193,6 +194,8 @@ public final class TwidereLinkify implements Constants {
int linkType = type; int linkType = type;
if (span instanceof AcctMentionSpan) { if (span instanceof AcctMentionSpan) {
linkType = LINK_TYPE_USER_ACCT; linkType = LINK_TYPE_USER_ACCT;
} else if (span instanceof HashtagSpan) {
linkType = LINK_TYPE_HASHTAG;
} else if (accountKey != null && USER_TYPE_FANFOU_COM.equals(accountKey.getHost())) { } else if (accountKey != null && USER_TYPE_FANFOU_COM.equals(accountKey.getHost())) {
// Fix search path // Fix search path
if (url.startsWith("/")) { if (url.startsWith("/")) {

View File

@ -24,6 +24,7 @@ import android.text.Spanned
import android.text.style.URLSpan import android.text.style.URLSpan
import org.mariotaku.twidere.model.SpanItem import org.mariotaku.twidere.model.SpanItem
import org.mariotaku.twidere.text.AcctMentionSpan import org.mariotaku.twidere.text.AcctMentionSpan
import org.mariotaku.twidere.text.HashtagSpan
import org.mariotaku.twidere.text.ZeroWidthSpan import org.mariotaku.twidere.text.ZeroWidthSpan
val SpanItem.length: Int get() = end - start val SpanItem.length: Int get() = end - start
@ -39,6 +40,10 @@ fun Array<SpanItem>.applyTo(spannable: Spannable) {
spannable.setSpan(AcctMentionSpan(span.link), span.start, span.end, spannable.setSpan(AcctMentionSpan(span.link), span.start, span.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
SpanItem.SpanType.HASHTAG -> {
spannable.setSpan(HashtagSpan(span.link), span.start, span.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
else -> { else -> {
spannable.setSpan(URLSpan(span.link), span.start, span.end, spannable.setSpan(URLSpan(span.link), span.start, span.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

View File

@ -38,6 +38,7 @@ import org.mariotaku.twidere.model.util.ParcelableLocationUtils
import org.mariotaku.twidere.model.util.ParcelableMediaUtils import org.mariotaku.twidere.model.util.ParcelableMediaUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag
import org.mariotaku.twidere.text.AcctMentionSpan import org.mariotaku.twidere.text.AcctMentionSpan
import org.mariotaku.twidere.text.HashtagSpan
import org.mariotaku.twidere.util.HtmlBuilder import org.mariotaku.twidere.util.HtmlBuilder
import org.mariotaku.twidere.util.HtmlSpanBuilder import org.mariotaku.twidere.util.HtmlSpanBuilder
import org.mariotaku.twidere.util.InternalTwitterContentUtils.getMediaUrl import org.mariotaku.twidere.util.InternalTwitterContentUtils.getMediaUrl
@ -278,8 +279,9 @@ internal fun findByOrigRange(spans: Array<SpanItem>, start: Int, end: Int): List
internal inline val CharSequence.spanItems get() = (this as? Spanned)?.let { text -> internal inline val CharSequence.spanItems get() = (this as? Spanned)?.let { text ->
text.getSpans(0, length, URLSpan::class.java).mapToArray { text.getSpans(0, length, URLSpan::class.java).mapToArray {
val item = it.toSpanItem(text) val item = it.toSpanItem(text)
if (it is AcctMentionSpan) { when (it) {
item.type = SpanItem.SpanType.ACCT_MENTION is AcctMentionSpan -> item.type = SpanItem.SpanType.ACCT_MENTION
is HashtagSpan -> item.type = SpanItem.SpanType.HASHTAG
} }
return@mapToArray item return@mapToArray item
} }

View File

@ -29,6 +29,7 @@ import org.mariotaku.twidere.extension.model.api.spanItems
import org.mariotaku.twidere.model.* import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag import org.mariotaku.twidere.model.util.ParcelableStatusUtils.addFilterFlag
import org.mariotaku.twidere.text.AcctMentionSpan import org.mariotaku.twidere.text.AcctMentionSpan
import org.mariotaku.twidere.text.HashtagSpan
import org.mariotaku.twidere.util.HtmlEscapeHelper import org.mariotaku.twidere.util.HtmlEscapeHelper
import org.mariotaku.twidere.util.HtmlSpanBuilder import org.mariotaku.twidere.util.HtmlSpanBuilder
import org.mariotaku.twidere.util.emoji.EmojioneTranslator import org.mariotaku.twidere.util.emoji.EmojioneTranslator
@ -132,21 +133,33 @@ private fun calculateDisplayTextRange(text: String, spans: Array<SpanItem>?, med
object MastodonSpanProcessor : HtmlSpanBuilder.SpanProcessor { object MastodonSpanProcessor : HtmlSpanBuilder.SpanProcessor {
override fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int): Boolean { override fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int,
info: HtmlSpanBuilder.TagInfo?): Boolean {
val unescaped = HtmlEscapeHelper.unescape(String(buffer, start, len)) val unescaped = HtmlEscapeHelper.unescape(String(buffer, start, len))
text.append(EmojioneTranslator.translate(unescaped)) text.append(EmojioneTranslator.translate(unescaped))
return true return true
} }
override fun applySpan(text: Editable, start: Int, end: Int, info: HtmlSpanBuilder.TagInfo): Boolean { override fun applySpan(text: Editable, start: Int, end: Int, info: HtmlSpanBuilder.TagInfo): Boolean {
val clsAttr = info.getAttribute("class") ?: return false val clsAttr = info.getAttribute("class")?.split(" ") ?: emptyList()
when {
"tag" in clsAttr || info.isTag(text) -> {
text.setSpan(HashtagSpan(text.substring(start, end)), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
"mention" in clsAttr -> {
val hrefAttr = info.getAttribute("href") ?: return false val hrefAttr = info.getAttribute("href") ?: return false
// Is mention or hashtag
if ("mention" !in clsAttr.split(" ")) return false
if (text[start] != '@') return false if (text[start] != '@') return false
text.setSpan(AcctMentionSpan(text.substring(start + 1, end), Uri.parse(hrefAttr).host), text.setSpan(AcctMentionSpan(text.substring(start + 1, end), Uri.parse(hrefAttr).host),
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return true return true
} }
}
return false
}
fun HtmlSpanBuilder.TagInfo.isTag(text: CharSequence): Boolean {
return start > 0 && text[start - 1] == '#'
}
} }

View File

@ -653,7 +653,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
val status = extras.getParcelable<ParcelableStatus>(EXTRA_STATUS) val status = extras.getParcelable<ParcelableStatus>(EXTRA_STATUS)
if (status.account_key.host != accountKey.host) { if (status.account_key.host != accountKey.host) {
val composeIntent = Intent(fragment.context, ComposeActivity::class.java) val composeIntent = Intent(fragment.context, ComposeActivity::class.java)
composeIntent.putExtra(Intent.EXTRA_TEXT, " ${LinkCreator.getStatusWebLink(status)}") composeIntent.putExtra(Intent.EXTRA_TEXT, "${status.text_plain } ${LinkCreator.getStatusWebLink(status)}")
composeIntent.putExtra(EXTRA_ACCOUNT_KEY, accountKey) composeIntent.putExtra(EXTRA_ACCOUNT_KEY, accountKey)
composeIntent.putExtra(EXTRA_SELECTION, 0) composeIntent.putExtra(EXTRA_SELECTION, 0)
fragment.startActivity(composeIntent) fragment.startActivity(composeIntent)

View File

@ -0,0 +1,28 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.text
import android.text.style.URLSpan
/**
* Created by mariotaku on 2017/5/26.
*/
class HashtagSpan(value: String) : URLSpan(value)

View File

@ -100,7 +100,7 @@ object HtmlSpanBuilder {
* @param start Start index of text to append in [buffer] * @param start Start index of text to append in [buffer]
* @param len Length of text to append in [buffer] * @param len Length of text to append in [buffer]
*/ */
fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int): Boolean = false fun appendText(text: Editable, buffer: CharArray, start: Int, len: Int, info: TagInfo?): Boolean = false
/** /**
* @param text Text to apply span from [info] * @param text Text to apply span from [info]
@ -112,7 +112,8 @@ object HtmlSpanBuilder {
} }
data class TagInfo(val start: Int, val name: String, val attributes: Map<String, String>?) { data class TagInfo(val start: Int, val name: String, val attributes: Map<String, String>?,
val parent: TagInfo? = null) {
val nameLower = name.toLowerCase(Locale.US) val nameLower = name.toLowerCase(Locale.US)
@ -138,11 +139,12 @@ object HtmlSpanBuilder {
) : AbstractSimpleMarkupHandler() { ) : AbstractSimpleMarkupHandler() {
private val sb = SpannableStringBuilder() private val sb = SpannableStringBuilder()
private var tagInfo = ArrayList<TagInfo>() private val tagStack = ArrayList<TagInfo>()
private var lastTag: TagInfo? = null private var lastTag: TagInfo? = null
override fun handleText(buffer: CharArray, offset: Int, len: Int, line: Int, col: Int) { override fun handleText(buffer: CharArray, offset: Int, len: Int, line: Int, col: Int) {
var cur = offset var cur = offset
val lastTag = this.lastTag
while (cur < offset + len) { while (cur < offset + len) {
// Find first line break // Find first line break
var lineBreakIndex = cur var lineBreakIndex = cur
@ -150,27 +152,27 @@ object HtmlSpanBuilder {
if (buffer[lineBreakIndex] == '\n') break if (buffer[lineBreakIndex] == '\n') break
lineBreakIndex++ lineBreakIndex++
} }
if (!(processor?.appendText(sb, buffer, cur, lineBreakIndex - cur) ?: false)) { if (!(processor?.appendText(sb, buffer, cur, lineBreakIndex - cur, lastTag) ?: false)) {
sb.append(HtmlEscapeHelper.unescape(String(buffer, cur, lineBreakIndex - cur))) sb.append(HtmlEscapeHelper.unescape(String(buffer, cur, lineBreakIndex - cur)))
} }
cur = lineBreakIndex + 1 cur = lineBreakIndex + 1
} }
lastTag = null this.lastTag = null
} }
override fun handleCloseElement(elementName: String, line: Int, col: Int) { override fun handleCloseElement(elementName: String, line: Int, col: Int) {
val lastIndex = lastIndexOfTag(tagInfo, elementName) val lastIndex = lastIndexOfTag(tagStack, elementName)
if (lastIndex == -1) return if (lastIndex == -1) return
val info = tagInfo[lastIndex] val info = tagStack[lastIndex]
applyTag(sb, info.start, sb.length, info, processor) applyTag(sb, info.start, sb.length, info, processor)
tagInfo.removeAt(lastIndex) tagStack.removeAt(lastIndex)
lastTag = info lastTag = info
} }
override fun handleOpenElement(elementName: String, attributes: Map<String, String>?, override fun handleOpenElement(elementName: String, attributes: Map<String, String>?,
line: Int, col: Int) { line: Int, col: Int) {
val info = TagInfo(sb.length, elementName, attributes) val info = TagInfo(sb.length, elementName, attributes, tagStack.lastOrNull())
tagInfo.add(info) tagStack.add(info)
// Mastodon case, insert 2 breaks between two <p> tag // Mastodon case, insert 2 breaks between two <p> tag
if ("p" == info.nameLower && "p" == lastTag?.nameLower) { if ("p" == info.nameLower && "p" == lastTag?.nameLower) {
@ -181,7 +183,7 @@ object HtmlSpanBuilder {
@Throws(ParseException::class) @Throws(ParseException::class)
override fun handleStandaloneElement(elementName: String, attributes: Map<String, String>?, override fun handleStandaloneElement(elementName: String, attributes: Map<String, String>?,
minimized: Boolean, line: Int, col: Int) { minimized: Boolean, line: Int, col: Int) {
val info = TagInfo(sb.length, elementName, attributes) val info = TagInfo(sb.length, elementName, attributes, tagStack.lastOrNull())
applyTag(sb, info.start, sb.length, info, processor) applyTag(sb, info.start, sb.length, info, processor)
} }