2018-08-22 01:35:54 +02:00
|
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
|
|
import android.graphics.Color
|
|
|
|
|
import android.graphics.Typeface
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.text.Layout
|
|
|
|
|
import android.text.Spannable
|
|
|
|
|
import android.text.SpannableStringBuilder
|
|
|
|
|
import android.text.Spanned
|
|
|
|
|
import android.text.style.AlignmentSpan
|
|
|
|
|
import android.text.style.BackgroundColorSpan
|
|
|
|
|
import android.text.style.ForegroundColorSpan
|
|
|
|
|
import android.text.style.RelativeSizeSpan
|
2018-08-22 07:11:25 +02:00
|
|
|
|
import android.util.SparseBooleanArray
|
2018-08-22 01:35:54 +02:00
|
|
|
|
import jp.juggler.subwaytooter.ActMain
|
|
|
|
|
import jp.juggler.subwaytooter.App1
|
|
|
|
|
import jp.juggler.subwaytooter.Pref
|
|
|
|
|
import jp.juggler.subwaytooter.R
|
2018-08-22 07:11:25 +02:00
|
|
|
|
import jp.juggler.subwaytooter.api.entity.EntityIdLong
|
|
|
|
|
import jp.juggler.subwaytooter.api.entity.TootMention
|
2018-08-22 03:05:54 +02:00
|
|
|
|
import jp.juggler.subwaytooter.span.*
|
2018-08-22 01:35:54 +02:00
|
|
|
|
import jp.juggler.subwaytooter.table.HighlightWord
|
|
|
|
|
import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan
|
|
|
|
|
import java.util.regex.Pattern
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
// 指定した文字数までの部分文字列を返す
|
|
|
|
|
private fun String.safeSubstring(count : Int, offset : Int = 0) = when {
|
|
|
|
|
offset + count <= length -> this.substring(offset, count)
|
|
|
|
|
else -> this.substring(offset, length)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
object MisskeySyntaxHighlighter {
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val symbolMap = SparseBooleanArray().apply {
|
|
|
|
|
for(c in "=+-*/%~^&|><!?") {
|
|
|
|
|
this.put(c.toInt(), true)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// 識別子に対して既存の名前と一致するか調べるようになったので、もはやソートの必要はない
|
|
|
|
|
private val keywords = HashSet<String>().apply {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val _keywords = arrayOf(
|
|
|
|
|
"true",
|
|
|
|
|
"false",
|
|
|
|
|
"null",
|
|
|
|
|
"nil",
|
|
|
|
|
"undefined",
|
|
|
|
|
"void",
|
|
|
|
|
"var",
|
|
|
|
|
"const",
|
|
|
|
|
"let",
|
|
|
|
|
"mut",
|
|
|
|
|
"dim",
|
|
|
|
|
"if",
|
|
|
|
|
"then",
|
|
|
|
|
"else",
|
|
|
|
|
"switch",
|
|
|
|
|
"match",
|
|
|
|
|
"case",
|
|
|
|
|
"default",
|
|
|
|
|
"for",
|
|
|
|
|
"each",
|
|
|
|
|
"in",
|
|
|
|
|
"while",
|
|
|
|
|
"loop",
|
|
|
|
|
"continue",
|
|
|
|
|
"break",
|
|
|
|
|
"do",
|
|
|
|
|
"goto",
|
|
|
|
|
"next",
|
|
|
|
|
"end",
|
|
|
|
|
"sub",
|
|
|
|
|
"throw",
|
|
|
|
|
"try",
|
|
|
|
|
"catch",
|
|
|
|
|
"finally",
|
|
|
|
|
"enum",
|
|
|
|
|
"delegate",
|
|
|
|
|
"function",
|
|
|
|
|
"func",
|
|
|
|
|
"fun",
|
|
|
|
|
"fn",
|
|
|
|
|
"return",
|
|
|
|
|
"yield",
|
|
|
|
|
"async",
|
|
|
|
|
"await",
|
|
|
|
|
"require",
|
|
|
|
|
"include",
|
|
|
|
|
"import",
|
|
|
|
|
"imports",
|
|
|
|
|
"export",
|
|
|
|
|
"exports",
|
|
|
|
|
"from",
|
|
|
|
|
"as",
|
|
|
|
|
"using",
|
|
|
|
|
"use",
|
|
|
|
|
"internal",
|
|
|
|
|
"module",
|
|
|
|
|
"namespace",
|
|
|
|
|
"where",
|
|
|
|
|
"select",
|
|
|
|
|
"struct",
|
|
|
|
|
"union",
|
|
|
|
|
"new",
|
|
|
|
|
"delete",
|
|
|
|
|
"this",
|
|
|
|
|
"super",
|
|
|
|
|
"base",
|
|
|
|
|
"class",
|
|
|
|
|
"interface",
|
|
|
|
|
"abstract",
|
|
|
|
|
"static",
|
|
|
|
|
"public",
|
|
|
|
|
"private",
|
|
|
|
|
"protected",
|
|
|
|
|
"virtual",
|
|
|
|
|
"partial",
|
|
|
|
|
"override",
|
|
|
|
|
"extends",
|
|
|
|
|
"implements",
|
|
|
|
|
"constructor"
|
|
|
|
|
)
|
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
// lower
|
|
|
|
|
addAll(_keywords)
|
|
|
|
|
|
|
|
|
|
// UPPER
|
|
|
|
|
addAll(_keywords.map { k -> k.toUpperCase() })
|
|
|
|
|
|
|
|
|
|
// Snake
|
|
|
|
|
addAll(_keywords.map { k -> k[0].toUpperCase() + k.substring(1) })
|
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
add("NaN")
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class Token(
|
|
|
|
|
var length : Int,
|
2018-08-22 07:11:25 +02:00
|
|
|
|
var color : Int = 0,
|
|
|
|
|
val italic : Boolean = false,
|
|
|
|
|
val comment : Boolean = false
|
2018-08-22 01:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
private class Env(
|
2018-08-22 07:11:25 +02:00
|
|
|
|
var source : String,
|
|
|
|
|
var pos : Int,
|
|
|
|
|
var remain : String
|
2018-08-22 01:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val reLineComment = Pattern.compile("""\A//.*""")
|
|
|
|
|
private val reBlockComment = Pattern.compile("""\A/\*.*?\*/""", Pattern.DOTALL)
|
|
|
|
|
private val reNumber = Pattern.compile("""\A[+-]?[\d.]+""")
|
2018-08-22 15:06:58 +02:00
|
|
|
|
private val reLabel = Pattern.compile("""\A@([A-Z_-][A-Z0-9_-]*)""", Pattern.CASE_INSENSITIVE)
|
|
|
|
|
private val reKeyword =
|
|
|
|
|
Pattern.compile("""\A([A-Z_-][A-Z0-9_-]*)([ \t]*\()?""", Pattern.CASE_INSENSITIVE)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
|
|
|
|
private val elements = arrayOf(
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
// comment
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val match = reLineComment.matcher(env.remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
|
|
|
|
match.find() -> Token(length = match.end(), comment = true)
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// block comment
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val match = reBlockComment.matcher(env.remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
|
|
|
|
match.find() -> Token(length = match.end(), comment = true)
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// string
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
|
|
|
|
val beginChar = env.remain[0]
|
|
|
|
|
if(beginChar != '"' && beginChar != '`') return@arrayOf null
|
|
|
|
|
var len = 1
|
|
|
|
|
while(len < env.remain.length) {
|
|
|
|
|
val char = env.remain[len ++]
|
|
|
|
|
if(char == beginChar) {
|
|
|
|
|
break // end
|
|
|
|
|
} else if(char == '\n' || len >= env.remain.length) {
|
|
|
|
|
len = 0 // not string literal
|
|
|
|
|
break
|
|
|
|
|
} else if(char == '\\' && len < env.remain.length) {
|
|
|
|
|
++ len // \" では閉じないようにする
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when(len) {
|
|
|
|
|
0 -> null
|
|
|
|
|
else -> Token(length = len, color = 0xe96900)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// regexp
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
2018-08-22 01:35:54 +02:00
|
|
|
|
if(env.remain[0] != '/') return@arrayOf null
|
|
|
|
|
val regexp = StringBuilder()
|
|
|
|
|
var thisIsNotARegexp = false
|
|
|
|
|
var i = 1
|
|
|
|
|
while(i < env.remain.length) {
|
|
|
|
|
val char = env.remain[i ++]
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(char == '/') {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
break
|
|
|
|
|
} else if(char == '\n' || i >= env.remain.length) {
|
|
|
|
|
thisIsNotARegexp = true
|
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
regexp.append(char)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(char == '\\' && i < env.remain.length) {
|
|
|
|
|
regexp.append(env.remain[i ++])
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
|
|
|
|
thisIsNotARegexp -> null
|
|
|
|
|
regexp.isEmpty() -> null
|
|
|
|
|
regexp[0] == ' ' && regexp[regexp.length - 1] == ' ' -> null
|
|
|
|
|
else -> Token(length = regexp.length + 2, color = 0xe9003f)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// label
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// 直前に識別子があればNG
|
|
|
|
|
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
|
|
|
|
|
if(prev?.isLetterOrDigit() == true) return@arrayOf null
|
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val match = reLabel.matcher(env.remain)
|
|
|
|
|
if(! match.find()) return@arrayOf null
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
|
|
|
|
val end = match.end()
|
|
|
|
|
when{
|
|
|
|
|
// @user@host のように直後に@が続くのはNG
|
|
|
|
|
env.remain.length > end && env.remain[end] =='@' -> null
|
|
|
|
|
else->Token(length = match.end(), color = 0xe9003f)
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// number
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
|
|
|
|
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
|
2018-08-22 15:06:58 +02:00
|
|
|
|
if(prev?.isLetterOrDigit() == true) return@arrayOf null
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val match = reNumber.matcher(env.remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
|
|
|
|
match.find() -> Token(length = match.end(), color = 0xae81ff)
|
|
|
|
|
else -> null
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// method, property, keyword
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// 直前の文字が識別子に使えるなら識別子の開始とはみなさない
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
|
2018-08-22 15:06:58 +02:00
|
|
|
|
if(prev?.isLetterOrDigit() == true || prev == '_') return@arrayOf null
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
val match = reKeyword.matcher(env.remain)
|
|
|
|
|
if(! match.find()) return@arrayOf null
|
|
|
|
|
val kw = match.group(1)
|
|
|
|
|
val bracket = match.group(2)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
when {
|
|
|
|
|
// メソッド呼び出しは対照が変数かプロパティかに関わらずメソッドの色になる
|
|
|
|
|
bracket?.isNotEmpty() == true ->
|
|
|
|
|
Token(length = kw.length, color = 0x8964c1, italic = true)
|
|
|
|
|
|
|
|
|
|
// 変数や定数ではなくプロパティならプロパティの色になる
|
|
|
|
|
prev == '.' ->Token(length = kw.length, color = 0xa71d5d)
|
|
|
|
|
|
|
|
|
|
keywords.contains(kw) -> when(kw) {
|
|
|
|
|
|
|
|
|
|
// 定数
|
|
|
|
|
"true", "false", "null", "nil", "undefined" ,"NaN" ->
|
|
|
|
|
Token(length = kw.length, color = 0xae81ff)
|
|
|
|
|
|
|
|
|
|
// その他の予約語
|
|
|
|
|
else ->Token(length = kw.length, color = 0x2973b7)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 強調表示しないが、識別子単位で読み飛ばす
|
|
|
|
|
else -> Token(length = kw.length)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// symbol
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : Env ->
|
|
|
|
|
when {
|
|
|
|
|
symbolMap.get(env.remain[0].toInt(), false) ->
|
|
|
|
|
Token(length = 1, color = 0x42b983)
|
|
|
|
|
else ->
|
|
|
|
|
null
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fun parse(source : String) : SpannableStringBuilder {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val sb = SpannableStringBuilder()
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val env = Env(source = source, pos = 0, remain = source)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
fun push(pos : Int, token : Token) {
|
|
|
|
|
val end = pos + token.length
|
|
|
|
|
sb.append(source.substring(pos, end))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
env.pos = end
|
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
if(token.comment) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
sb.setSpan(
|
|
|
|
|
ForegroundColorSpan(Color.BLACK or 0x808000)
|
|
|
|
|
, pos, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
|
)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
} else {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
var c = token.color
|
|
|
|
|
if(c != 0) {
|
|
|
|
|
if(c < 0x1000000) {
|
|
|
|
|
c = c or Color.BLACK
|
|
|
|
|
}
|
|
|
|
|
sb.setSpan(
|
|
|
|
|
ForegroundColorSpan(c)
|
|
|
|
|
, pos, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if(token.italic) {
|
|
|
|
|
sb.setSpan(
|
|
|
|
|
CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC))
|
|
|
|
|
, pos, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var textToken : Token? = null
|
|
|
|
|
var textTokenStart = 0
|
2018-08-22 07:11:25 +02:00
|
|
|
|
fun closeTextToken() {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val token = textToken
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(token != null) {
|
|
|
|
|
token.length = env.pos - textTokenStart
|
|
|
|
|
push(textTokenStart, token)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
textToken = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
loop1@ while(env.remain.isNotEmpty()) {
|
|
|
|
|
for(el in elements) {
|
|
|
|
|
val token = el(env) ?: continue
|
|
|
|
|
closeTextToken()
|
2018-08-22 07:11:25 +02:00
|
|
|
|
push(env.pos, token)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
env.remain = source.substring(env.pos)
|
|
|
|
|
continue@loop1
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(textToken == null) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
textToken = Token(length = 0)
|
|
|
|
|
textTokenStart = env.pos
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
env.remain = source.substring(++ env.pos)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
closeTextToken()
|
|
|
|
|
|
|
|
|
|
return sb
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
object MisskeyMarkdownDecoder {
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val log = LogCategory("MisskeyMarkdownDecoder")
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
enum class NodeType {
|
|
|
|
|
Text,
|
|
|
|
|
Big,
|
|
|
|
|
Bold,
|
|
|
|
|
Title,
|
|
|
|
|
Url,
|
|
|
|
|
Link,
|
|
|
|
|
Mention,
|
|
|
|
|
Hashtag,
|
|
|
|
|
CodeBlock,
|
|
|
|
|
CodeInline,
|
|
|
|
|
Quote,
|
|
|
|
|
Emoji,
|
|
|
|
|
Search,
|
|
|
|
|
Motion
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private class Node(
|
|
|
|
|
var type : NodeType,
|
|
|
|
|
var sourceStart : Int,
|
|
|
|
|
var sourceLength : Int,
|
|
|
|
|
var data : ArrayList<String?>?
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private class ParserEnv(
|
|
|
|
|
val text : String
|
|
|
|
|
, var pos : Int
|
|
|
|
|
, var remain : String
|
2018-08-22 15:06:58 +02:00
|
|
|
|
) {
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
internal fun genNode1(
|
|
|
|
|
type : NodeType,
|
|
|
|
|
sourceLength : Int,
|
|
|
|
|
data : ArrayList<String?>?
|
2018-08-22 15:06:58 +02:00
|
|
|
|
) = Node(
|
2018-08-22 07:11:25 +02:00
|
|
|
|
type = type,
|
|
|
|
|
sourceStart = pos,
|
|
|
|
|
sourceLength = sourceLength,
|
|
|
|
|
data = data
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private fun simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
type : NodeType,
|
|
|
|
|
pattern : Pattern
|
2018-08-22 07:11:25 +02:00
|
|
|
|
) = { env : ParserEnv ->
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val matcher = pattern.matcher(env.remain)
|
|
|
|
|
when {
|
|
|
|
|
matcher.find() -> env.genNode1(
|
|
|
|
|
type
|
|
|
|
|
, matcher.end()
|
2018-08-22 07:11:25 +02:00
|
|
|
|
, arrayListOf(matcher.group(1))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
private val reLink =
|
|
|
|
|
Pattern.compile("""^\??\[([^\[\]]+?)]\((https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+?)\)""")
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val reCodeInline = Pattern.compile("""^`(.+?)`""")
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val reMention = Pattern.compile(
|
|
|
|
|
"""^@([a-z0-9_]+)(?:@([a-z0-9.\-]+[a-z0-9]))?"""
|
2018-08-22 15:06:58 +02:00
|
|
|
|
, Pattern.CASE_INSENSITIVE
|
2018-08-22 07:11:25 +02:00
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val reHashtag = Pattern.compile("""^#([^\s]+)""")
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private val reMotion1 = Pattern.compile("""^\Q(((\E(.+?)\Q)))\E""")
|
|
|
|
|
private val reMotion2 = Pattern.compile("""^<motion>(.+?)</motion>""")
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
private val nodeParserList = arrayOf(
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
// 処理順序に意味があるので入れ替えないこと
|
|
|
|
|
// 記号列が長い順
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Big,
|
|
|
|
|
Pattern.compile("""^\Q***\E(.+?)\Q***\E""")
|
|
|
|
|
),
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Bold,
|
|
|
|
|
Pattern.compile("""^\Q**\E(.+?)\Q**\E""")
|
|
|
|
|
),
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Title,
|
|
|
|
|
Pattern.compile("""^[【\[](.+?)[】\]]\n""")
|
|
|
|
|
),
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Url,
|
|
|
|
|
Pattern.compile("""^(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
|
|
|
|
|
),
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : ParserEnv ->
|
|
|
|
|
val matcher = reLink.matcher(env.remain)
|
|
|
|
|
when {
|
|
|
|
|
matcher.find() -> env.genNode1(
|
|
|
|
|
NodeType.Link
|
|
|
|
|
, matcher.end()
|
|
|
|
|
, arrayListOf(
|
|
|
|
|
matcher.group(1) // title
|
|
|
|
|
, matcher.group(2) // url
|
|
|
|
|
, env.remain[0].toString() // silent なら "?" になる
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{ env : ParserEnv ->
|
|
|
|
|
val matcher = reMention.matcher(env.remain)
|
|
|
|
|
when {
|
|
|
|
|
matcher.find() -> env.genNode1(
|
|
|
|
|
NodeType.Mention
|
|
|
|
|
, matcher.end()
|
|
|
|
|
, arrayListOf(
|
|
|
|
|
matcher.group(1) // username
|
|
|
|
|
, matcher.group(2) // host
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
{ env : ParserEnv ->
|
|
|
|
|
val matcher = reHashtag.matcher(env.remain)
|
|
|
|
|
when {
|
|
|
|
|
matcher.find() -> when {
|
|
|
|
|
|
|
|
|
|
// 先頭以外では直前に空白が必要らしい
|
|
|
|
|
env.pos > 0 && ! CharacterGroup.isWhitespace(
|
|
|
|
|
env.text[env.pos - 1].toInt()
|
|
|
|
|
) -> null
|
|
|
|
|
|
|
|
|
|
else -> env.genNode1(
|
|
|
|
|
NodeType.Hashtag
|
|
|
|
|
, matcher.end()
|
|
|
|
|
, arrayListOf(
|
|
|
|
|
matcher.group(1) // 先頭の#を含まないハッシュタグ
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.CodeBlock,
|
2018-08-22 15:06:58 +02:00
|
|
|
|
Pattern.compile("""^```(.+?)```""", Pattern.DOTALL)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
),
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : ParserEnv ->
|
|
|
|
|
val matcher = reCodeInline.matcher(env.remain)
|
|
|
|
|
when {
|
|
|
|
|
matcher.find() -> when {
|
|
|
|
|
|
|
|
|
|
// インラインコードは内部にある文字を含むと認識されない。理由は謎
|
|
|
|
|
matcher.group(1).contains('´') -> null
|
|
|
|
|
|
|
|
|
|
else -> env.genNode1(
|
|
|
|
|
NodeType.CodeInline
|
|
|
|
|
, matcher.end()
|
|
|
|
|
, arrayListOf(
|
|
|
|
|
matcher.group(1)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
},
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Quote,
|
|
|
|
|
Pattern.compile("""^"([\s\S]+?)\n"""")
|
|
|
|
|
),
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Emoji,
|
|
|
|
|
Pattern.compile("""^:([a-zA-Z0-9+-_]+):""")
|
|
|
|
|
),
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
simpleParser(
|
2018-08-22 01:35:54 +02:00
|
|
|
|
NodeType.Search,
|
|
|
|
|
Pattern.compile(
|
2018-08-22 07:11:25 +02:00
|
|
|
|
"""^(.+?)[ ](検索|\[検索]|Search|\[Search])(\n|${'$'})"""
|
2018-08-22 15:06:58 +02:00
|
|
|
|
, Pattern.CASE_INSENSITIVE
|
2018-08-22 01:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
{ env : ParserEnv ->
|
|
|
|
|
var found = false
|
|
|
|
|
var matcher = reMotion1.matcher(env.remain)
|
|
|
|
|
if(matcher.find()) {
|
|
|
|
|
found = true
|
|
|
|
|
} else {
|
|
|
|
|
matcher = reMotion2.matcher(env.remain)
|
|
|
|
|
if(matcher.find()) {
|
|
|
|
|
found = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
when(found) {
|
|
|
|
|
true -> env.genNode1(
|
|
|
|
|
NodeType.Motion
|
|
|
|
|
, matcher.end()
|
|
|
|
|
, arrayListOf(
|
|
|
|
|
matcher.group(1) // 先頭の#を含まないハッシュタグ
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
2018-08-22 03:05:54 +02:00
|
|
|
|
private val reStartEmptyLines = """\A(?:[ ]*?[\x0d\x0a]+)+""".toRegex()
|
|
|
|
|
private val reEndEmptyLines = """[\s\x0d\x0a]+\z""".toRegex()
|
2018-08-22 07:11:25 +02:00
|
|
|
|
private fun trimBlock(s : String?) : String? {
|
|
|
|
|
s ?: return null
|
2018-08-22 03:05:54 +02:00
|
|
|
|
return s
|
2018-08-22 07:11:25 +02:00
|
|
|
|
.replace(reStartEmptyLines, "")
|
|
|
|
|
.replace(reEndEmptyLines, "")
|
2018-08-22 03:05:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
private fun parse(source : String?) : ArrayList<Node> {
|
|
|
|
|
val result = ArrayList<Node>()
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
if(source != null) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
|
|
|
|
val env = ParserEnv(
|
|
|
|
|
text = source,
|
|
|
|
|
pos = 0,
|
|
|
|
|
remain = source
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
var textNode : Node? = null
|
|
|
|
|
fun closeTextNode() {
|
|
|
|
|
val node = textNode ?: return
|
|
|
|
|
val length = env.pos - node.sourceStart
|
|
|
|
|
if(length > 0) {
|
|
|
|
|
node.sourceLength = length
|
|
|
|
|
result.add(node)
|
|
|
|
|
}
|
|
|
|
|
textNode = null
|
|
|
|
|
}
|
|
|
|
|
//
|
|
|
|
|
loop1@ while(env.remain.isNotEmpty()) {
|
|
|
|
|
|
|
|
|
|
for(el in nodeParserList) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val node = el(env) ?: continue
|
2018-08-22 01:35:54 +02:00
|
|
|
|
closeTextNode()
|
2018-08-22 07:11:25 +02:00
|
|
|
|
result.add(node)
|
|
|
|
|
env.pos += node.sourceLength
|
2018-08-22 01:35:54 +02:00
|
|
|
|
env.remain = env.text.substring(env.pos)
|
|
|
|
|
continue@loop1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// テキストノードの開始
|
|
|
|
|
if(textNode == null) {
|
|
|
|
|
textNode = Node(
|
|
|
|
|
NodeType.Text,
|
|
|
|
|
env.pos,
|
|
|
|
|
0,
|
|
|
|
|
null
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
env.remain = env.text.substring(++ env.pos)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
closeTextNode()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
class SpannableStringBuilderEx : SpannableStringBuilder() {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
var mentions : ArrayList<TootMention>? = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun decodeMarkdown(options : DecodeOptions, src : String?) : SpannableStringBuilderEx {
|
|
|
|
|
val sb = SpannableStringBuilderEx()
|
|
|
|
|
val context = options.context ?: return sb
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
fun urlShorter(display_url : String, href : String) : CharSequence {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(options.isMediaAttachment(href)) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
@Suppress("NAME_SHADOWING")
|
|
|
|
|
val sb = SpannableStringBuilder()
|
|
|
|
|
sb.append(href)
|
|
|
|
|
val start = 0
|
|
|
|
|
val end = sb.length
|
|
|
|
|
sb.setSpan(
|
|
|
|
|
EmojiImageSpan(context, R.drawable.emj_1f5bc),
|
|
|
|
|
start,
|
|
|
|
|
end,
|
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
|
)
|
|
|
|
|
return sb
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val uri = Uri.parse(display_url)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
@Suppress("NAME_SHADOWING")
|
|
|
|
|
val sb = StringBuilder()
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(! display_url.startsWith("http")) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
sb.append(uri.scheme)
|
|
|
|
|
sb.append("://")
|
|
|
|
|
}
|
|
|
|
|
sb.append(uri.authority)
|
|
|
|
|
val a = uri.encodedPath
|
|
|
|
|
val q = uri.encodedQuery
|
|
|
|
|
val f = uri.encodedFragment
|
|
|
|
|
val remain = a + (if(q == null) "" else "?$q") + if(f == null) "" else "#$f"
|
|
|
|
|
if(remain.length > 10) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
sb.append(remain.safeSubstring(10))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
sb.append("…")
|
|
|
|
|
} else {
|
|
|
|
|
sb.append(remain)
|
|
|
|
|
}
|
|
|
|
|
return sb
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
return display_url
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if(src != null) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-08-22 03:05:54 +02:00
|
|
|
|
val font_bold = ActMain.timeline_font_bold
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
for(node in parse(src)) {
|
|
|
|
|
val nodeSource =
|
|
|
|
|
src.substring(node.sourceStart, node.sourceStart + node.sourceLength)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
var start = sb.length
|
|
|
|
|
val data = node.data
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
fun setSpan(span : Any) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val end = sb.length
|
|
|
|
|
sb.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
fun setHighlight() {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val list = options.highlightTrie?.matchList(sb, start, sb.length)
|
|
|
|
|
if(list != null) {
|
|
|
|
|
for(range in list) {
|
|
|
|
|
val word = HighlightWord.load(range.word)
|
|
|
|
|
if(word != null) {
|
|
|
|
|
options.hasHighlight = true
|
|
|
|
|
sb.setSpan(
|
|
|
|
|
HighlightSpan(word.color_fg, word.color_bg),
|
|
|
|
|
range.start,
|
|
|
|
|
range.end,
|
|
|
|
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
|
|
|
)
|
|
|
|
|
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
|
|
|
|
|
options.highlight_sound = word
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
fun appendText(text : CharSequence?, preventHighlight : Boolean = false) {
|
|
|
|
|
text ?: return
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
|
|
|
|
sb.append(text)
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(! preventHighlight) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
setHighlight()
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
fun appendTextCode(text : String?, preventHighlight : Boolean = false) {
|
|
|
|
|
text ?: return
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
|
|
|
|
sb.append(MisskeySyntaxHighlighter.parse(text))
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(! preventHighlight) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
setHighlight()
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
fun appendLink(text : String, url : String, allowShort : Boolean = false) {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
when {
|
|
|
|
|
text.isEmpty() -> return
|
2018-08-22 07:11:25 +02:00
|
|
|
|
! allowShort -> appendText(text, preventHighlight = true)
|
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
else -> {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val short = urlShorter(text, url)
|
|
|
|
|
appendText(short, preventHighlight = true)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val linkHelper = options.linkHelper
|
|
|
|
|
if(linkHelper != null) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
setSpan(
|
|
|
|
|
MyClickableSpan(
|
|
|
|
|
text,
|
|
|
|
|
url,
|
|
|
|
|
linkHelper.findAcctColor(url),
|
|
|
|
|
options.linkTag
|
|
|
|
|
)
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
setHighlight()
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when(node.type) {
|
|
|
|
|
|
|
|
|
|
NodeType.Url -> {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val url = data?.get(0)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(url?.isNotEmpty() == true) {
|
|
|
|
|
appendLink(url, url, allowShort = true)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
NodeType.Link -> {
|
|
|
|
|
val title = data?.get(0) ?: "?"
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val url = data?.get(1)
|
|
|
|
|
// val silent = data?.get(2)
|
|
|
|
|
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(url?.isNotEmpty() == true) {
|
|
|
|
|
appendLink(title, url)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Mention -> {
|
|
|
|
|
|
|
|
|
|
val username = data?.get(0) ?: ""
|
|
|
|
|
val host = data?.get(1) ?: ""
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val linkHelper = options.linkHelper
|
|
|
|
|
if(linkHelper == null) {
|
|
|
|
|
appendText(
|
|
|
|
|
when {
|
|
|
|
|
host.isEmpty() -> "@$username"
|
|
|
|
|
else -> "@$username@$host"
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
)
|
|
|
|
|
} else {
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val shortAcct = when {
|
|
|
|
|
host.isEmpty()
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|| host.equals(linkHelper.host, ignoreCase = true) ->
|
2018-08-22 07:11:25 +02:00
|
|
|
|
username
|
|
|
|
|
else ->
|
|
|
|
|
"$username@$host"
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
|
|
|
|
val userHost = when {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
host.isEmpty() -> linkHelper.host
|
|
|
|
|
else -> host
|
|
|
|
|
}
|
|
|
|
|
val userUrl = "https://$userHost/@$username"
|
|
|
|
|
|
|
|
|
|
var mentions = sb.mentions
|
2018-08-22 15:06:58 +02:00
|
|
|
|
if(mentions == null) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
mentions = ArrayList()
|
|
|
|
|
sb.mentions = mentions
|
|
|
|
|
}
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
|
|
|
|
if(mentions.find { it.acct == shortAcct } == null) {
|
|
|
|
|
mentions.add(
|
|
|
|
|
TootMention(
|
|
|
|
|
EntityIdLong(- 1L)
|
|
|
|
|
, userUrl
|
|
|
|
|
, shortAcct
|
|
|
|
|
, username
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
appendLink(
|
|
|
|
|
when {
|
|
|
|
|
Pref.bpMentionFullAcct(App1.pref) -> "@$username@$userHost"
|
|
|
|
|
else -> "@$shortAcct"
|
|
|
|
|
}
|
|
|
|
|
, userUrl
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 03:05:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Hashtag -> {
|
|
|
|
|
val linkHelper = options.linkHelper
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val tag = data?.get(0)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(tag?.isNotEmpty() == true && linkHelper != null) {
|
|
|
|
|
appendLink(
|
|
|
|
|
"#$tag",
|
|
|
|
|
"https://${linkHelper.host}/tags/" + tag.encodePercent()
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 03:05:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Emoji -> {
|
2018-08-22 03:05:54 +02:00
|
|
|
|
val code = data?.get(0)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(code?.isNotEmpty() == true) {
|
2018-08-22 03:05:54 +02:00
|
|
|
|
appendText(options.decodeEmoji(":$code:"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////
|
|
|
|
|
// 装飾インライン要素
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Text -> {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
appendText(nodeSource)
|
|
|
|
|
}
|
2018-08-22 03:05:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Big -> {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
appendText(data?.get(0))
|
2018-08-22 03:05:54 +02:00
|
|
|
|
setSpan(MisskeyBigSpan(font_bold))
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Motion -> {
|
2018-08-22 03:05:54 +02:00
|
|
|
|
val code = data?.get(0)
|
|
|
|
|
appendText(code)
|
|
|
|
|
setSpan(MisskeyMotionSpan(ActMain.timeline_font))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Bold -> {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
appendText(data?.get(0))
|
|
|
|
|
setSpan(CalligraphyTypefaceSpan(font_bold))
|
|
|
|
|
}
|
2018-08-22 03:05:54 +02:00
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.CodeInline -> {
|
2018-08-22 03:05:54 +02:00
|
|
|
|
appendTextCode(data?.get(0))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
setSpan(BackgroundColorSpan(0x40808080))
|
|
|
|
|
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
2018-08-22 03:05:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////
|
|
|
|
|
// ブロック要素
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
NodeType.Title -> {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-22 03:05:54 +02:00
|
|
|
|
appendText(trimBlock(data?.get(0)))
|
|
|
|
|
setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER))
|
|
|
|
|
setSpan(BackgroundColorSpan(0x20808080))
|
|
|
|
|
setSpan(RelativeSizeSpan(1.5f))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
appendText("\n")
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
NodeType.CodeBlock -> {
|
2018-08-22 03:05:54 +02:00
|
|
|
|
appendTextCode(trimBlock(data?.get(0)))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
setSpan(BackgroundColorSpan(0x40808080))
|
2018-08-22 03:05:54 +02:00
|
|
|
|
setSpan(RelativeSizeSpan(0.7f))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
2018-08-22 03:05:54 +02:00
|
|
|
|
appendText("\n")
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
|
|
|
|
NodeType.Quote -> {
|
2018-08-22 03:05:54 +02:00
|
|
|
|
appendText(trimBlock(data?.get(0)))
|
|
|
|
|
setSpan(BackgroundColorSpan(0x20808080))
|
2018-08-22 01:35:54 +02:00
|
|
|
|
setSpan(CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC)))
|
|
|
|
|
appendText("\n")
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 07:11:25 +02:00
|
|
|
|
NodeType.Search -> {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val text = data?.get(0)
|
2018-08-22 03:05:54 +02:00
|
|
|
|
val kw_start = sb.length // キーワードの開始位置
|
|
|
|
|
appendText(text)
|
|
|
|
|
appendText(" ")
|
|
|
|
|
start = sb.length // 検索リンクの開始位置
|
|
|
|
|
appendLink(
|
|
|
|
|
context.getString(R.string.search),
|
2018-08-22 07:11:25 +02:00
|
|
|
|
"https://www.google.co.jp/search?q=" + (text
|
|
|
|
|
?: "Subway Tooter").encodePercent()
|
|
|
|
|
)
|
|
|
|
|
sb.setSpan(
|
|
|
|
|
RelativeSizeSpan(1.2f),
|
|
|
|
|
kw_start,
|
|
|
|
|
sb.length,
|
|
|
|
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
2018-08-22 03:05:54 +02:00
|
|
|
|
)
|
|
|
|
|
appendText("\n")
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 末尾の空白を取り除く
|
|
|
|
|
var end = sb.length
|
|
|
|
|
while(end > 0 && HTMLDecoder.isWhitespaceOrLineFeed(sb[end - 1].toInt())) -- end
|
|
|
|
|
if(end < sb.length) sb.delete(end, sb.length)
|
|
|
|
|
}
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sb
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|