improve hashtag detection when compositing new status
This commit is contained in:
parent
091d54255d
commit
e653bfd467
|
@ -1,11 +1,12 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.util.MisskeyMarkdownDecoder
|
||||
import jp.juggler.util.notEmptyOrThrow
|
||||
import jp.juggler.util.parseString
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.regex.Pattern
|
||||
|
||||
open class TootTag(
|
||||
// The hashtag, not including the preceding #
|
||||
|
@ -20,11 +21,11 @@ open class TootTag(
|
|||
)
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
// 検索結果のhashtagリストから生成する
|
||||
fun parseTootTagList(parser:TootParser,array : JSONArray?) : ArrayList<TootTag> {
|
||||
fun parseTootTagList(parser : TootParser, array : JSONArray?) : ArrayList<TootTag> {
|
||||
val result = ArrayList<TootTag>()
|
||||
if( parser.serviceType == ServiceType.MISSKEY){
|
||||
if(parser.serviceType == ServiceType.MISSKEY) {
|
||||
if(array != null) {
|
||||
for(i in 0 until array.length()) {
|
||||
val sv = array.parseString(i)
|
||||
|
@ -34,7 +35,7 @@ open class TootTag(
|
|||
}
|
||||
}
|
||||
|
||||
}else {
|
||||
} else {
|
||||
if(array != null) {
|
||||
for(i in 0 until array.length()) {
|
||||
val sv = array.parseString(i)
|
||||
|
@ -48,5 +49,61 @@ open class TootTag(
|
|||
return result
|
||||
}
|
||||
|
||||
// \p{L} : アルファベット (Letter)。
|
||||
// Ll(小文字)、Lm(擬似文字)、Lo(その他の文字)、Lt(タイトル文字)、Lu(大文字アルファベット)を含む
|
||||
// \p{M} : 記号 (Mark)
|
||||
// \p{Nd} : 10 進数字 (Decimal number)
|
||||
// \p{Pc} : 連結用句読記号 (Connector punctuation)
|
||||
|
||||
// rubyの [:word:] : 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
|
||||
private val w = """\p{L}\p{M}\p{Nd}\p{Pc}"""
|
||||
|
||||
// rubyの [:alpha:] : 英字 (Letter | Mark)
|
||||
private val a = """\p{L}\p{M}"""
|
||||
|
||||
// 2019/7/20 https://github.com/tootsuite/mastodon/pull/11363/files
|
||||
private val reTagMastodon : Pattern =
|
||||
Pattern.compile("""(?:^|[^\w)])#([_$w][·_$w]*[·_$a][·_$w]*[_$w]|[_$w]*[$a][_$w]*)""")
|
||||
|
||||
// https://medium.com/@alice/some-article#.abcdef123 => タグにならない
|
||||
// https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit => タグにならない
|
||||
// #aesthetic => #aesthetic
|
||||
// #3d => #3d
|
||||
// #l33ts35k => #l33ts35k
|
||||
// #world2016 => #world2016
|
||||
// #_test => #_test
|
||||
// #test_ => #test_
|
||||
// #one·two·three· => 末尾の・はタグに含まれない。#one·two·three までがハッシュタグになる。
|
||||
// #0123456' => 数字だけのハッシュタグはタグとして認識されない。
|
||||
// #000_000 => 認識される。
|
||||
//
|
||||
|
||||
// タグに使えない文字
|
||||
// 入力補完用なのでやや緩め
|
||||
private val reCharsNotTagMastodon = Pattern.compile("""[^·_$w$a]""")
|
||||
private val reCharsNotTagMisskey = Pattern.compile("""[\s.,!?'${'"'}:/\[\]【】]""")
|
||||
|
||||
// find hashtags in content text(raw)
|
||||
// returns null if hashtags not found, or ArrayList of String (tag without #)
|
||||
fun findHashtags(src : String, isMisskey : Boolean) : ArrayList<String>? =
|
||||
if(isMisskey) {
|
||||
MisskeyMarkdownDecoder.findHashtags(src)
|
||||
} else {
|
||||
var result : ArrayList<String>? = null
|
||||
val m = reTagMastodon.matcher(src)
|
||||
while(m.find()) {
|
||||
if(result == null) result = ArrayList()
|
||||
result.add(m.group(1))
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fun isValid(src : String, isMisskey : Boolean) =
|
||||
if(isMisskey) {
|
||||
! reCharsNotTagMisskey.matcher(src).find()
|
||||
} else {
|
||||
! reCharsNotTagMastodon.matcher(src).find()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,20 +5,20 @@ enum class TootVisibility(
|
|||
, val order : Int // 公開範囲の広い方とWeb設定に合わせる方が大きい
|
||||
, val strMastodon : String
|
||||
, val strMisskey : String
|
||||
, @Suppress("unused") val isLocal :Boolean = false
|
||||
, @Suppress("unused") val isLocal : Boolean = false
|
||||
) {
|
||||
|
||||
// IDは下書き保存などで永続化するので、リリース後は変更しないこと!
|
||||
|
||||
|
||||
// アカウント設定に合わせる。
|
||||
AccountSetting(-1, 200, strMastodon = "account_setting", strMisskey = "account_setting"),
|
||||
|
||||
AccountSetting(- 1, 200, strMastodon = "account_setting", strMisskey = "account_setting"),
|
||||
|
||||
// WebUIの設定に合わせる。
|
||||
WebSetting(0, 100, strMastodon = "web_setting", strMisskey = "web_setting"),
|
||||
|
||||
// 公開TLに流す
|
||||
Public(1, 90, strMastodon = "public", strMisskey = "public"),
|
||||
LocalPublic(6, 85, strMastodon = "public", strMisskey = "local-public",isLocal = true),
|
||||
LocalPublic(6, 85, strMastodon = "public", strMisskey = "local-public", isLocal = true),
|
||||
|
||||
// LTL,FTLには表示されない。
|
||||
// フォロワーのホームには表示される。
|
||||
|
@ -26,7 +26,7 @@ enum class TootVisibility(
|
|||
// (Mastodon)タグTLには出ない。
|
||||
// (Misskey)タグTLには出る。
|
||||
UnlistedHome(2, 80, strMastodon = "unlisted", strMisskey = "home"),
|
||||
LocalHome(6, 75, strMastodon = "unlisted", strMisskey = "local-home",isLocal = true),
|
||||
LocalHome(6, 75, strMastodon = "unlisted", strMisskey = "local-home", isLocal = true),
|
||||
|
||||
// 未フォローには見せない。
|
||||
// (Mastodon)フォロワーのHTLに出る。
|
||||
|
@ -36,8 +36,8 @@ enum class TootVisibility(
|
|||
// (Misskey)非ログインの閲覧者から見たのタグTLには出るが内容は隠される。「あの人はエロゲのタグで何か話してた」とか分かっちゃう。
|
||||
// (Misskey)非ログインの閲覧者から見たのプロフには出るが内容は隠される。「あの人は寝てるはずの時間に何か投稿してた」とか分かっちゃう。
|
||||
PrivateFollowers(3, 70, strMastodon = "private", strMisskey = "followers"),
|
||||
LocalFollowers(3, 65, strMastodon = "private", strMisskey = "local-followers",isLocal = true),
|
||||
|
||||
LocalFollowers(3, 65, strMastodon = "private", strMisskey = "local-followers", isLocal = true),
|
||||
|
||||
// 指定したユーザにのみ送信する。
|
||||
// (Misskey)送信先ユーザのIDをリストで指定する。投稿前にユーザの存在確認を行う機会がある。
|
||||
// (Misskey)送信先ユーザが1以上ならspecified、0ならprivateを指定する。
|
||||
|
@ -65,24 +65,46 @@ enum class TootVisibility(
|
|||
}
|
||||
}
|
||||
|
||||
fun isTagAllowed(isMisskey : Boolean) =
|
||||
if(isMisskey)
|
||||
when(this) {
|
||||
// 以下の二つの指定ではチェックを行えないので許可扱いにする
|
||||
AccountSetting, WebSetting -> true
|
||||
// Misskeyは公開とホームに書いたタグはタグTLに出る
|
||||
Public, LocalPublic, UnlistedHome, LocalHome -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
else
|
||||
when(this) {
|
||||
// 以下の二つの指定ではチェックを行えないので許可扱いにする
|
||||
AccountSetting, WebSetting -> true
|
||||
// Mastodon は公開のみタグTLに出る
|
||||
Public, LocalPublic -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun parseMastodon(a : String?) : TootVisibility? {
|
||||
for(v in TootVisibility.values()) {
|
||||
for(v in values()) {
|
||||
if(v.strMastodon == a) return v
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun parseMisskey(a : String?,localOnly:Boolean = false ) : TootVisibility? {
|
||||
for(v in TootVisibility.values()) {
|
||||
if(v.strMisskey == a){
|
||||
if( localOnly ){
|
||||
when(v){
|
||||
fun parseMisskey(a : String?, localOnly : Boolean = false) : TootVisibility? {
|
||||
for(v in values()) {
|
||||
if(v.strMisskey == a) {
|
||||
if(localOnly) {
|
||||
when(v) {
|
||||
Public -> return LocalPublic
|
||||
UnlistedHome -> return LocalHome
|
||||
PrivateFollowers -> return LocalFollowers
|
||||
else->{}
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
|
@ -92,7 +114,7 @@ enum class TootVisibility(
|
|||
}
|
||||
|
||||
fun fromId(id : Int) : TootVisibility? {
|
||||
for(v in TootVisibility.values()) {
|
||||
for(v in values()) {
|
||||
if(v.id == id) return v
|
||||
}
|
||||
return null
|
||||
|
@ -102,12 +124,12 @@ enum class TootVisibility(
|
|||
sv ?: return null
|
||||
|
||||
// 新しい方式ではenumのID
|
||||
for(v in TootVisibility.values()) {
|
||||
for(v in values()) {
|
||||
if(v.id.toString() == sv) return v
|
||||
}
|
||||
|
||||
// 古い方式ではマストドンの公開範囲文字列かweb_setting
|
||||
for(v in TootVisibility.values()) {
|
||||
for(v in values()) {
|
||||
if(v.strMastodon == sv) return v
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ private val bracketsMap = HashMap<Char, Int>().apply {
|
|||
}
|
||||
private val bracketsMapUrlSafe = HashMap<Char, Int>().apply {
|
||||
brackets.forEach {
|
||||
if( "([".contains(it[0]) ) return@forEach
|
||||
if("([".contains(it[0])) return@forEach
|
||||
put(it[0], 1)
|
||||
put(it[1], - 1)
|
||||
}
|
||||
|
@ -132,24 +132,24 @@ private val bracketsMapUrlSafe = HashMap<Char, Int>().apply {
|
|||
|
||||
// 末尾の余計な」や(を取り除く。
|
||||
// 例えば「#タグ」 とか (#タグ)
|
||||
fun String.removeOrphanedBrackets(urlSafe:Boolean =false) : String {
|
||||
fun String.removeOrphanedBrackets(urlSafe : Boolean = false) : String {
|
||||
var last = 0
|
||||
val nests = when(urlSafe){
|
||||
true->this.map {
|
||||
val nests = when(urlSafe) {
|
||||
true -> this.map {
|
||||
last += bracketsMapUrlSafe[it] ?: 0
|
||||
last
|
||||
}
|
||||
else->this.map {
|
||||
else -> this.map {
|
||||
|
||||
last += bracketsMap[it] ?: 0
|
||||
last
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// first position of unmatched close
|
||||
var pos = nests.indexOfFirst { it < 0 }
|
||||
if(pos != - 1) return substring(0, pos)
|
||||
|
||||
|
||||
// last position of unmatched open
|
||||
pos = nests.indexOfLast { it == 0 }
|
||||
return substring(0, pos + 1)
|
||||
|
@ -1237,6 +1237,7 @@ object MisskeyMarkdownDecoder {
|
|||
NodeType.QUOTE_BLOCK, NodeType.QUOTE_INLINE -> 1
|
||||
else -> 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// マークダウン要素の出現位置
|
||||
|
@ -1727,6 +1728,27 @@ object MisskeyMarkdownDecoder {
|
|||
)
|
||||
}
|
||||
|
||||
// 入力テキストからタグを抽出するために使う
|
||||
// #を含まないタグ文字列のリスト、またはnullを返す
|
||||
fun findHashtags(src : String?) : ArrayList<String>? {
|
||||
try {
|
||||
if(src != null) {
|
||||
val root = Node(NodeType.ROOT, emptyArray(), null)
|
||||
NodeParseEnv(root, src, 0, src.length).parseInside()
|
||||
val result = ArrayList<String>()
|
||||
fun track(n : Node) {
|
||||
if(n.type == NodeType.HASHTAG) result.add(n.args[0])
|
||||
n.childNodes.forEach { track(n) }
|
||||
}
|
||||
track(root)
|
||||
if(result.isNotEmpty()) return result
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.e(ex, "findHashtags failed.")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// このファイルのエントリーポイント
|
||||
fun decodeMarkdown(options : DecodeOptions, src : String?) =
|
||||
SpannableStringBuilderEx().apply {
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.json.JSONObject
|
|||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.min
|
||||
|
||||
class PostHelper(
|
||||
private val activity : AppCompatActivity,
|
||||
|
@ -41,20 +42,7 @@ class PostHelper(
|
|||
companion object {
|
||||
private val log = LogCategory("PostHelper")
|
||||
|
||||
// [:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
|
||||
// [:alpha:] 英字 (Letter | Mark)
|
||||
|
||||
private const val word = "[_\\p{L}\\p{M}\\p{Nd}\\p{Pc}]"
|
||||
private const val alpha = "[_\\p{L}\\p{M}]"
|
||||
|
||||
private val reTag = Pattern.compile(
|
||||
"(?:^|[^/)\\w])#($word*$alpha$word*)", Pattern.CASE_INSENSITIVE
|
||||
)
|
||||
|
||||
private val reCharsNotTag = Pattern.compile("[・\\s\\-+.,:;/]")
|
||||
private val reCharsNotEmoji = Pattern.compile("[^0-9A-Za-z_-]")
|
||||
|
||||
|
||||
}
|
||||
|
||||
interface PostCompleteCallback {
|
||||
|
@ -177,10 +165,9 @@ class PostHelper(
|
|||
|
||||
if(! bConfirmTag) {
|
||||
|
||||
if(! account.isMisskey
|
||||
&& visibility != TootVisibility.Public
|
||||
&& reTag.matcher(content).find()
|
||||
) {
|
||||
val tags = TootTag.findHashtags(content,account.isMisskey)
|
||||
if( tags != null && !visibility.isTagAllowed(isMisskey = account.isMisskey) ){
|
||||
log.d("findHashtags ${tags.joinToString(",")}")
|
||||
AlertDialog.Builder(activity)
|
||||
.setCancelable(true)
|
||||
.setMessage(R.string.hashtag_and_visibility_not_match)
|
||||
|
@ -736,7 +723,7 @@ class PostHelper(
|
|||
}
|
||||
|
||||
val part = src.substring(last_sharp + 1, end)
|
||||
if(reCharsNotTag.matcher(part).find()) {
|
||||
if( ! TootTag.isValid(part, isMisskey) ){
|
||||
checkEmoji(et, src)
|
||||
return
|
||||
}
|
||||
|
@ -998,7 +985,7 @@ class PostHelper(
|
|||
|
||||
val src = et.text ?: ""
|
||||
val src_length = src.length
|
||||
val end = Math.min(src_length, et.selectionEnd)
|
||||
val end = min(src_length, et.selectionEnd)
|
||||
val start = src.lastIndexOf(':', end - 1)
|
||||
if(start == - 1 || end - start < 1) return@EmojiPicker
|
||||
|
||||
|
@ -1026,8 +1013,8 @@ class PostHelper(
|
|||
|
||||
val src = et.text ?: ""
|
||||
val src_length = src.length
|
||||
val start = Math.min(src_length, et.selectionStart)
|
||||
val end = Math.min(src_length, et.selectionEnd)
|
||||
val start = min(src_length, et.selectionStart)
|
||||
val end = min(src_length, et.selectionEnd)
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
.append(src.subSequence(0, start))
|
||||
|
|
Loading…
Reference in New Issue