fixed #850
This commit is contained in:
parent
647767e436
commit
039a022593
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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("/")) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
val hrefAttr = info.getAttribute("href") ?: return false
|
when {
|
||||||
// Is mention or hashtag
|
"tag" in clsAttr || info.isTag(text) -> {
|
||||||
if ("mention" !in clsAttr.split(" ")) return false
|
text.setSpan(HashtagSpan(text.substring(start, end)), start, end,
|
||||||
if (text[start] != '@') return false
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
text.setSpan(AcctMentionSpan(text.substring(start + 1, end), Uri.parse(hrefAttr).host),
|
return true
|
||||||
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
}
|
||||||
return true
|
"mention" in clsAttr -> {
|
||||||
|
val hrefAttr = info.getAttribute("href") ?: return false
|
||||||
|
if (text[start] != '@') return false
|
||||||
|
text.setSpan(AcctMentionSpan(text.substring(start + 1, end), Uri.parse(hrefAttr).host),
|
||||||
|
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun HtmlSpanBuilder.TagInfo.isTag(text: CharSequence): Boolean {
|
||||||
|
return start > 0 && text[start - 1] == '#'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue