2018-08-22 01:35:54 +02:00
|
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
2018-08-24 02:26:45 +02:00
|
|
|
|
import android.content.Context
|
2018-08-22 01:35:54 +02:00
|
|
|
|
import android.graphics.Color
|
|
|
|
|
import android.graphics.Typeface
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.text.SpannableStringBuilder
|
|
|
|
|
import android.text.Spanned
|
|
|
|
|
import android.text.style.BackgroundColorSpan
|
|
|
|
|
import android.text.style.ForegroundColorSpan
|
2018-08-24 02:26:45 +02:00
|
|
|
|
import android.util.SparseArray
|
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-11-21 08:42:21 +01:00
|
|
|
|
import jp.juggler.subwaytooter.span.EmojiImageSpan
|
|
|
|
|
import jp.juggler.subwaytooter.span.HighlightSpan
|
|
|
|
|
import jp.juggler.subwaytooter.span.MisskeyBigSpan
|
|
|
|
|
import jp.juggler.subwaytooter.span.MyClickableSpan
|
2018-08-22 01:35:54 +02:00
|
|
|
|
import jp.juggler.subwaytooter.table.HighlightWord
|
|
|
|
|
import java.util.regex.Pattern
|
2018-11-21 08:42:21 +01:00
|
|
|
|
import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan
|
|
|
|
|
import java.util.*
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 07:04:55 +02:00
|
|
|
|
// 配列中の要素をラムダ式で変換して、戻り値が非nullならそこで処理を打ち切る
|
|
|
|
|
private inline fun <T, V> Array<out T>.firstNonNull(predicate : (T) -> V?) : V? {
|
|
|
|
|
for(element in this) return predicate(element) ?: continue
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
class SpanPos(
|
|
|
|
|
var start : Int,
|
|
|
|
|
var end : Int,
|
|
|
|
|
val span : Any
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
class SpanList {
|
|
|
|
|
val list = LinkedList<SpanPos>()
|
|
|
|
|
|
|
|
|
|
fun addFirst(start : Int, end : Int, span : Any) = when {
|
|
|
|
|
start == end -> {
|
|
|
|
|
// empty span allowed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start > end -> {
|
|
|
|
|
MisskeyMarkdownDecoder.log.e("SpanList.add: range error! start=$start,end=$end,span=$span")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
list.addFirst(SpanPos(start, end, span))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun addLast(start : Int, end : Int, span : Any) = when {
|
|
|
|
|
start == end -> {
|
|
|
|
|
// empty span allowed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
start > end -> {
|
|
|
|
|
MisskeyMarkdownDecoder.log.e("SpanList.add: range error! start=$start,end=$end,span=$span")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
list.addLast(SpanPos(start, end, span))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun addWithOffset(src : Iterable<SpanPos>, offset : Int) {
|
|
|
|
|
for(sp in src) {
|
|
|
|
|
addLast(sp.start + offset, sp.end + offset, sp.span)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun insert(offset : Int, length : Int) {
|
|
|
|
|
for(sp in list) {
|
|
|
|
|
when {
|
|
|
|
|
sp.end <= offset -> {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sp.start <= offset -> {
|
|
|
|
|
sp.end += length
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
sp.start += length
|
|
|
|
|
sp.end += length
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// ```code``` マークダウン内部ではプログラムっぽい何かの文法強調表示が行われる
|
2018-08-22 01:35:54 +02:00
|
|
|
|
object MisskeySyntaxHighlighter {
|
|
|
|
|
|
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-24 07:04:55 +02:00
|
|
|
|
|
|
|
|
|
// 識別子に対して既存の名前と一致するか調べるようになったので、もはやソートの必要はない
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val symbolMap = SparseBooleanArray().apply {
|
|
|
|
|
for(c in "=+-*/%~^&|><!?") {
|
|
|
|
|
this.put(c.toInt(), true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// 文字列リテラルの開始文字のマップ
|
2018-08-24 07:04:55 +02:00
|
|
|
|
private val stringStart = SparseBooleanArray().apply {
|
|
|
|
|
for(c in "\"'`") {
|
|
|
|
|
this.put(c.toInt(), true)
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class Token(
|
2018-08-24 07:04:55 +02:00
|
|
|
|
val length : Int,
|
|
|
|
|
val color : Int = 0,
|
2018-08-22 07:11:25 +02:00
|
|
|
|
val italic : Boolean = false,
|
|
|
|
|
val comment : Boolean = false
|
2018-08-22 01:35:54 +02:00
|
|
|
|
)
|
|
|
|
|
|
2018-08-24 07:04:55 +02:00
|
|
|
|
private class Env(val source : String) {
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
// 出力先2
|
|
|
|
|
val spanList = SpanList()
|
2018-08-24 07:04:55 +02:00
|
|
|
|
|
|
|
|
|
// 残り部分
|
|
|
|
|
var remain : String = source
|
|
|
|
|
private set
|
|
|
|
|
|
|
|
|
|
// スキャン位置
|
|
|
|
|
var pos : Int = 0
|
|
|
|
|
set(value) {
|
|
|
|
|
field = value
|
|
|
|
|
remain = source.substring(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun push(start : Int, token : Token) {
|
|
|
|
|
val end = start + token.length
|
|
|
|
|
|
|
|
|
|
if(token.comment) {
|
2018-11-21 08:42:21 +01:00
|
|
|
|
spanList.addLast(start, end, ForegroundColorSpan(Color.BLACK or 0x808000))
|
2018-08-24 07:04:55 +02:00
|
|
|
|
} else {
|
|
|
|
|
var c = token.color
|
|
|
|
|
if(c != 0) {
|
|
|
|
|
if(c < 0x1000000) {
|
|
|
|
|
c = c or Color.BLACK
|
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
spanList.addLast(start, end, ForegroundColorSpan(c))
|
2018-08-24 07:04:55 +02:00
|
|
|
|
}
|
|
|
|
|
if(token.italic) {
|
2018-11-21 08:42:21 +01:00
|
|
|
|
spanList.addLast(
|
|
|
|
|
start,
|
|
|
|
|
end,
|
2018-08-24 07:04:55 +02:00
|
|
|
|
CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC))
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun parse() : SpanList {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
|
|
|
|
|
var lastEnd = 0
|
|
|
|
|
fun closeTextToken(textEnd : Int) {
|
|
|
|
|
val length = textEnd - lastEnd
|
|
|
|
|
if(length > 0) {
|
|
|
|
|
push(lastEnd, Token(length = length))
|
|
|
|
|
lastEnd = textEnd
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while(remain.isNotEmpty()) {
|
|
|
|
|
val token = elements.firstNonNull { this.it() }
|
|
|
|
|
if(token == null) {
|
|
|
|
|
++ pos
|
|
|
|
|
} else {
|
|
|
|
|
closeTextToken(pos)
|
|
|
|
|
push(pos, token)
|
|
|
|
|
this.pos += token.length
|
|
|
|
|
lastEnd = pos
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
closeTextToken(pos)
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
return spanList
|
2018-08-24 07:04:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
|
|
2018-08-24 07:04:55 +02:00
|
|
|
|
private val elements = arrayOf<Env.() -> Token?>(
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
// comment
|
2018-08-24 07:04:55 +02:00
|
|
|
|
{
|
|
|
|
|
val match = reLineComment.matcher(remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
! match.find() -> null
|
|
|
|
|
else -> Token(length = match.end(), comment = true)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// block comment
|
2018-08-24 07:04:55 +02:00
|
|
|
|
{
|
|
|
|
|
val match = reBlockComment.matcher(remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
! match.find() -> null
|
|
|
|
|
else -> Token(length = match.end(), comment = true)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// string
|
2018-08-24 07:04:55 +02:00
|
|
|
|
{
|
|
|
|
|
val beginChar = remain[0]
|
2018-08-24 14:37:47 +02:00
|
|
|
|
if(! stringStart[beginChar.toInt()]) return@arrayOf null
|
2018-08-22 07:11:25 +02:00
|
|
|
|
var len = 1
|
2018-08-24 07:04:55 +02:00
|
|
|
|
while(len < remain.length) {
|
|
|
|
|
val char = remain[len ++]
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(char == beginChar) {
|
|
|
|
|
break // end
|
2018-08-24 07:04:55 +02:00
|
|
|
|
} else if(char == '\n' || len >= remain.length) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
len = 0 // not string literal
|
|
|
|
|
break
|
2018-08-24 07:04:55 +02:00
|
|
|
|
} else if(char == '\\' && len < remain.length) {
|
2018-08-22 07:11:25 +02:00
|
|
|
|
++ 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-24 07:04:55 +02:00
|
|
|
|
{
|
|
|
|
|
if(remain[0] != '/') return@arrayOf null
|
2018-08-22 01:35:54 +02:00
|
|
|
|
val regexp = StringBuilder()
|
2018-08-24 07:04:55 +02:00
|
|
|
|
var notClosed = false
|
2018-08-22 01:35:54 +02:00
|
|
|
|
var i = 1
|
2018-08-24 07:04:55 +02:00
|
|
|
|
while(i < remain.length) {
|
|
|
|
|
val char = remain[i ++]
|
2018-08-22 07:11:25 +02:00
|
|
|
|
if(char == '/') {
|
2018-08-22 01:35:54 +02:00
|
|
|
|
break
|
2018-08-24 07:04:55 +02:00
|
|
|
|
} else if(char == '\n' || i >= remain.length) {
|
|
|
|
|
notClosed = true
|
2018-08-22 01:35:54 +02:00
|
|
|
|
break
|
|
|
|
|
} else {
|
|
|
|
|
regexp.append(char)
|
2018-08-24 07:04:55 +02:00
|
|
|
|
if(char == '\\' && i < remain.length) {
|
|
|
|
|
regexp.append(remain[i ++])
|
2018-08-22 07:11:25 +02:00
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
notClosed -> null
|
2018-08-22 07:11:25 +02:00
|
|
|
|
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-24 07:04:55 +02:00
|
|
|
|
{
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// 直前に識別子があればNG
|
2018-08-24 07:04:55 +02:00
|
|
|
|
val prev = if(pos <= 0) null else source[pos - 1]
|
2018-08-22 15:06:58 +02:00
|
|
|
|
if(prev?.isLetterOrDigit() == true) return@arrayOf null
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-08-24 07:04:55 +02:00
|
|
|
|
val match = reLabel.matcher(remain)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
if(! match.find()) return@arrayOf null
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
val end = match.end()
|
2018-08-24 02:26:45 +02:00
|
|
|
|
when {
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// @user@host のように直後に@が続くのはNG
|
2018-08-24 07:04:55 +02:00
|
|
|
|
remain.length > end && remain[end] == '@' -> null
|
2018-08-24 02:26:45 +02:00
|
|
|
|
else -> Token(length = match.end(), color = 0xe9003f)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// number
|
2018-08-24 07:04:55 +02:00
|
|
|
|
{
|
|
|
|
|
val prev = if(pos <= 0) null else source[pos - 1]
|
2018-08-22 15:06:58 +02:00
|
|
|
|
if(prev?.isLetterOrDigit() == true) return@arrayOf null
|
2018-08-24 07:04:55 +02:00
|
|
|
|
val match = reNumber.matcher(remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
! match.find() -> null
|
|
|
|
|
else -> Token(length = match.end(), color = 0xae81ff)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// method, property, keyword
|
2018-08-24 07:04:55 +02:00
|
|
|
|
{
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// 直前の文字が識別子に使えるなら識別子の開始とはみなさない
|
2018-08-24 07:04:55 +02:00
|
|
|
|
val prev = if(pos <= 0) null else source[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-24 07:04:55 +02:00
|
|
|
|
val match = reKeyword.matcher(remain)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
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 {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
// メソッド呼び出しは対象が変数かプロパティかに関わらずメソッドの色になる
|
2018-08-22 15:06:58 +02:00
|
|
|
|
bracket?.isNotEmpty() == true ->
|
|
|
|
|
Token(length = kw.length, color = 0x8964c1, italic = true)
|
|
|
|
|
|
|
|
|
|
// 変数や定数ではなくプロパティならプロパティの色になる
|
2018-08-24 02:26:45 +02:00
|
|
|
|
prev == '.' -> Token(length = kw.length, color = 0xa71d5d)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-24 07:04:55 +02:00
|
|
|
|
// 予約語ではない
|
|
|
|
|
// 強調表示しないが、識別子単位で読み飛ばす
|
|
|
|
|
! keywords.contains(kw) -> Token(length = kw.length)
|
|
|
|
|
|
|
|
|
|
else -> when(kw) {
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// 定数
|
2018-08-24 02:26:45 +02:00
|
|
|
|
"true", "false", "null", "nil", "undefined", "NaN" ->
|
2018-08-22 15:06:58 +02:00
|
|
|
|
Token(length = kw.length, color = 0xae81ff)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-08-22 15:06:58 +02:00
|
|
|
|
// その他の予約語
|
2018-08-24 02:26:45 +02:00
|
|
|
|
else -> Token(length = kw.length, color = 0x2973b7)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// symbol
|
2018-08-24 07:04:55 +02:00
|
|
|
|
{
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 07:04:55 +02:00
|
|
|
|
symbolMap.get(remain[0].toInt(), false) ->
|
2018-08-22 07:11:25 +02:00
|
|
|
|
Token(length = 1, color = 0x42b983)
|
2018-08-24 07:04:55 +02:00
|
|
|
|
else -> null
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2018-08-24 07:04:55 +02:00
|
|
|
|
fun parse(source : String) = Env(source = source).parse()
|
|
|
|
|
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
object MisskeyMarkdownDecoder {
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
internal val log = LogCategory("MisskeyMarkdownDecoder")
|
|
|
|
|
|
|
|
|
|
internal val DEBUG = false
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// デコード結果にはメンションの配列を含む。TootStatusのパーサがこれを回収する。
|
2018-11-21 08:42:21 +01:00
|
|
|
|
class SpannableStringBuilderEx(
|
2018-08-24 02:26:45 +02:00
|
|
|
|
var mentions : ArrayList<TootMention>? = null
|
2018-11-21 08:42:21 +01:00
|
|
|
|
) : SpannableStringBuilder()
|
|
|
|
|
|
|
|
|
|
// ブロック要素は始端と終端の空行を除去したい
|
|
|
|
|
private val reStartEmptyLines = """\A(?:[ ]*?[\x0d\x0a]+)+""".toRegex()
|
|
|
|
|
private val reEndEmptyLines = """[\s\x0d\x0a]+\z""".toRegex()
|
|
|
|
|
private fun trimBlock(s : String) =
|
|
|
|
|
s.replace(reStartEmptyLines, "")
|
|
|
|
|
.replace(reEndEmptyLines, "")
|
|
|
|
|
|
|
|
|
|
private fun shortenUrl(display_url : String) : String {
|
|
|
|
|
return try {
|
|
|
|
|
val uri = Uri.parse(display_url)
|
|
|
|
|
|
|
|
|
|
val sbTmp = StringBuilder()
|
|
|
|
|
if(! display_url.startsWith("http")) {
|
|
|
|
|
sbTmp.append(uri.scheme)
|
|
|
|
|
sbTmp.append("://")
|
|
|
|
|
}
|
|
|
|
|
sbTmp.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) {
|
|
|
|
|
sbTmp.append(remain.safeSubstring(10))
|
|
|
|
|
sbTmp.append("…")
|
|
|
|
|
} else {
|
|
|
|
|
sbTmp.append(remain)
|
|
|
|
|
}
|
|
|
|
|
sbTmp.toString()
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
display_url
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// マークダウン要素のデコード時に使う作業変数をまとめたクラス
|
2018-11-21 08:42:21 +01:00
|
|
|
|
internal class SpanOutputEnv(
|
|
|
|
|
val options : DecodeOptions,
|
|
|
|
|
val sb : SpannableStringBuilderEx
|
|
|
|
|
) {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
|
|
|
|
|
val context : Context = options.context ?: error("missing context")
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val font_bold = ActMain.timeline_font_bold
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val linkHelper : LinkHelper? = options.linkHelper
|
2018-11-21 08:42:21 +01:00
|
|
|
|
var spanList = SpanList()
|
2018-08-24 14:37:47 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
var start = 0
|
|
|
|
|
|
|
|
|
|
fun prepareMentions() : ArrayList<TootMention> {
|
|
|
|
|
var mentions = sb.mentions
|
|
|
|
|
if(mentions != null) return mentions
|
|
|
|
|
mentions = ArrayList()
|
|
|
|
|
sb.mentions = mentions
|
|
|
|
|
return mentions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun fireRender(node : Node) : SpanList {
|
|
|
|
|
val spanList = SpanList()
|
|
|
|
|
this.spanList = spanList
|
|
|
|
|
this.start = sb.length
|
|
|
|
|
val render = node.type.render
|
|
|
|
|
this.render(node)
|
|
|
|
|
return spanList
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun fireRenderChildNodes(parent : Node) : SpanList {
|
|
|
|
|
val parent_result = this.spanList
|
|
|
|
|
parent.childNodes.forEach {
|
|
|
|
|
val child_result = fireRender(it)
|
|
|
|
|
parent_result.list.addAll(child_result.list)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
this.spanList = parent_result
|
|
|
|
|
return parent_result
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// 直前の文字が改行文字でなければ改行する
|
2018-08-24 02:26:45 +02:00
|
|
|
|
fun closePreviousBlock() {
|
|
|
|
|
if(start > 0 && sb[start - 1] != '\n') {
|
|
|
|
|
sb.append('\n')
|
|
|
|
|
start = sb.length
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun closeBlock() {
|
|
|
|
|
if(sb.length > 0 && sb[sb.length - 1] != '\n') {
|
|
|
|
|
sb.append('\n')
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
private fun applyHighlight(start : Int, end : Int) {
|
|
|
|
|
val list = options.highlightTrie?.matchList(sb, start, end)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
if(list != null) {
|
|
|
|
|
for(range in list) {
|
|
|
|
|
val word = HighlightWord.load(range.word)
|
|
|
|
|
if(word != null) {
|
|
|
|
|
options.hasHighlight = true
|
2018-11-21 08:42:21 +01:00
|
|
|
|
spanList.addLast(
|
2018-08-24 02:26:45 +02:00
|
|
|
|
range.start,
|
|
|
|
|
range.end,
|
2018-11-21 08:42:21 +01:00
|
|
|
|
HighlightSpan(word.color_fg, word.color_bg)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
|
|
|
|
|
options.highlight_sound = word
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// テキストを追加する
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun appendText(text : String, decodeEmoji : Boolean = false) {
|
|
|
|
|
val start = sb.length
|
|
|
|
|
if(decodeEmoji) {
|
|
|
|
|
sb.append(options.decodeEmoji(text))
|
|
|
|
|
} else {
|
|
|
|
|
sb.append(text)
|
|
|
|
|
}
|
|
|
|
|
applyHighlight(start, sb.length)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// URL中のテキストを追加する
|
|
|
|
|
private fun appendLinkText(display_url : String, href : String) {
|
|
|
|
|
when {
|
|
|
|
|
// 添付メディアのURLなら絵文字に変えてしまう
|
|
|
|
|
options.isMediaAttachment(href) -> {
|
|
|
|
|
// リンクの一部に絵文字がある場合、絵文字スパンをセットしてからリンクをセットする
|
|
|
|
|
val start = sb.length
|
|
|
|
|
sb.append(href)
|
|
|
|
|
spanList.addFirst(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
EmojiImageSpan(context, R.drawable.emj_1f5bc_fe0f)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> appendText(shortenUrl(display_url))
|
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// リンクを追加する
|
2018-08-24 02:26:45 +02:00
|
|
|
|
fun appendLink(text : String, url : String, allowShort : Boolean = false) {
|
2018-11-21 08:42:21 +01:00
|
|
|
|
when {
|
|
|
|
|
allowShort -> appendLinkText(text, url)
|
|
|
|
|
else -> appendText(text)
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val linkHelper = options.linkHelper
|
|
|
|
|
if(linkHelper != null) {
|
2018-11-21 08:42:21 +01:00
|
|
|
|
// リンクの一部にハイライトがある場合、リンクをセットしてからハイライトをセットしないとクリック判定がおかしくなる。
|
|
|
|
|
spanList.addFirst(
|
|
|
|
|
start, sb.length, MyClickableSpan(
|
2018-08-24 14:37:47 +02:00
|
|
|
|
text
|
|
|
|
|
, url
|
|
|
|
|
, linkHelper.findAcctColor(url)
|
|
|
|
|
, options.linkTag
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
2018-08-25 13:18:08 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun <T> hashSetOf(vararg values : T) = HashSet<T>().apply { addAll(values) }
|
2018-08-25 13:18:08 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
enum class Node2Type(
|
|
|
|
|
val allowInside : Set<Node2Type> = emptySet(),
|
|
|
|
|
val allowInsideAll : Boolean = false,
|
|
|
|
|
val render : SpanOutputEnv.(Node) -> Unit
|
|
|
|
|
) {
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
|
|
// 入れ子なし
|
|
|
|
|
|
|
|
|
|
TEXT(
|
|
|
|
|
render = { appendText(it.args[0],decodeEmoji=true) }
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
EMOJI(
|
|
|
|
|
render = {
|
|
|
|
|
val code = it.args[0]
|
|
|
|
|
if(code.isNotEmpty()) {
|
|
|
|
|
appendText(":$code:",decodeEmoji=true)
|
2018-08-25 13:18:08 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
MENTION(
|
|
|
|
|
render = {
|
|
|
|
|
val username = it.args[0]
|
|
|
|
|
val host = it.args[1]
|
|
|
|
|
val linkHelper = linkHelper
|
|
|
|
|
if(linkHelper == null) {
|
|
|
|
|
appendText(
|
|
|
|
|
when {
|
|
|
|
|
host.isEmpty() -> "@$username"
|
|
|
|
|
else -> "@$username@$host"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
val shortAcct = when {
|
|
|
|
|
host.isEmpty()
|
|
|
|
|
|| host.equals(linkHelper.host, ignoreCase = true) ->
|
|
|
|
|
username
|
|
|
|
|
else ->
|
|
|
|
|
"$username@$host"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val userHost = when {
|
|
|
|
|
host.isEmpty() -> linkHelper.host
|
|
|
|
|
else -> host
|
|
|
|
|
}
|
|
|
|
|
val userUrl = "https://$userHost/@$username"
|
|
|
|
|
|
|
|
|
|
val mentions = prepareMentions()
|
|
|
|
|
|
|
|
|
|
if(mentions.find { m -> m.acct == shortAcct } == null) {
|
|
|
|
|
mentions.add(
|
|
|
|
|
TootMention(
|
|
|
|
|
EntityIdLong(- 1L)
|
|
|
|
|
, userUrl
|
|
|
|
|
, shortAcct
|
|
|
|
|
, username
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendLink(
|
|
|
|
|
when {
|
|
|
|
|
Pref.bpMentionFullAcct(App1.pref) -> "@$username@$userHost"
|
|
|
|
|
else -> "@$shortAcct"
|
|
|
|
|
}
|
|
|
|
|
, userUrl
|
|
|
|
|
)
|
|
|
|
|
}
|
2018-08-25 13:18:08 +02:00
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
HASHTAG(
|
|
|
|
|
render = {
|
|
|
|
|
val linkHelper = linkHelper
|
|
|
|
|
val tag = it.args[0]
|
|
|
|
|
if(tag.isNotEmpty() && linkHelper != null) {
|
|
|
|
|
appendLink(
|
|
|
|
|
"#$tag",
|
|
|
|
|
"https://${linkHelper.host}/tags/" + tag.encodePercent()
|
|
|
|
|
)
|
|
|
|
|
}
|
2018-08-25 13:18:08 +02:00
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
CODE_INLINE(
|
|
|
|
|
render = {
|
|
|
|
|
val text = it.args[0]
|
|
|
|
|
val sp = MisskeySyntaxHighlighter.parse(text)
|
|
|
|
|
appendText(text)
|
|
|
|
|
spanList.addWithOffset(sp.list, start)
|
|
|
|
|
spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080))
|
|
|
|
|
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
URL(
|
|
|
|
|
render = {
|
|
|
|
|
val url = it.args[0]
|
|
|
|
|
if(url.isNotEmpty()) {
|
|
|
|
|
appendLink(url, url, allowShort = true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
CODE_BLOCK(
|
2018-08-25 13:18:08 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
render = {
|
|
|
|
|
closePreviousBlock()
|
|
|
|
|
|
|
|
|
|
val text = trimBlock(it.args[0])
|
|
|
|
|
val sp = MisskeySyntaxHighlighter.parse(text)
|
|
|
|
|
appendText(text)
|
|
|
|
|
spanList.addWithOffset(sp.list, start)
|
|
|
|
|
spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080))
|
|
|
|
|
spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(0.7f))
|
|
|
|
|
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
|
|
|
|
closeBlock()
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
QUOTE_INLINE(
|
|
|
|
|
render = {
|
|
|
|
|
val text = trimBlock(it.args[0])
|
|
|
|
|
appendText(text)
|
|
|
|
|
spanList.addLast(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
android.text.style.BackgroundColorSpan(0x20808080)
|
|
|
|
|
)
|
|
|
|
|
spanList.addLast(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
CalligraphyTypefaceSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC))
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
SEARCH(
|
|
|
|
|
render = {
|
|
|
|
|
closePreviousBlock()
|
|
|
|
|
|
|
|
|
|
val text = it.args[0]
|
|
|
|
|
val kw_start = sb.length // キーワードの開始位置
|
|
|
|
|
appendText(text)
|
|
|
|
|
appendText(" ")
|
|
|
|
|
start = sb.length // 検索リンクの開始位置
|
|
|
|
|
|
|
|
|
|
appendLink(
|
|
|
|
|
context.getString(jp.juggler.subwaytooter.R.string.search),
|
|
|
|
|
"https://www.google.co.jp/search?q=${text.encodePercent()}"
|
|
|
|
|
)
|
|
|
|
|
spanList.addLast(kw_start, sb.length, android.text.style.RelativeSizeSpan(1.2f))
|
|
|
|
|
|
|
|
|
|
closeBlock()
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////
|
|
|
|
|
// 入れ子あり
|
|
|
|
|
|
|
|
|
|
// インライン要素、装飾のみ
|
|
|
|
|
|
|
|
|
|
BIG(
|
|
|
|
|
allowInside = hashSetOf(MENTION, HASHTAG, EMOJI),
|
|
|
|
|
render = {
|
|
|
|
|
val start = this.start
|
|
|
|
|
fireRenderChildNodes(it)
|
|
|
|
|
spanList.addLast(start, sb.length, MisskeyBigSpan(font_bold))
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
BOLD(
|
|
|
|
|
allowInside = hashSetOf(MENTION, HASHTAG, EMOJI),
|
|
|
|
|
render = {
|
|
|
|
|
val start = this.start
|
|
|
|
|
fireRenderChildNodes(it)
|
|
|
|
|
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(font_bold))
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
MOTION(
|
|
|
|
|
allowInside = hashSetOf(BOLD, MENTION, HASHTAG, EMOJI),
|
|
|
|
|
render = {
|
|
|
|
|
val start = this.start
|
|
|
|
|
fireRenderChildNodes(it)
|
|
|
|
|
spanList.addFirst(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
jp.juggler.subwaytooter.span.MisskeyMotionSpan(jp.juggler.subwaytooter.ActMain.timeline_font)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// リンクなどのデータを扱う要素
|
|
|
|
|
|
|
|
|
|
LINK(
|
|
|
|
|
allowInside = hashSetOf(BIG, BOLD, MOTION, EMOJI),
|
|
|
|
|
render = {
|
|
|
|
|
val url = it.args[1]
|
|
|
|
|
// val silent = data?.get(2)
|
|
|
|
|
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
|
|
|
|
|
|
|
|
|
|
if(url.isNotEmpty()) {
|
|
|
|
|
val start = this.start
|
|
|
|
|
fireRenderChildNodes(it)
|
|
|
|
|
val linkHelper = options.linkHelper
|
|
|
|
|
if(linkHelper != null) {
|
|
|
|
|
spanList.addFirst(
|
|
|
|
|
start, sb.length,
|
|
|
|
|
MyClickableSpan(
|
|
|
|
|
sb.substring(start, sb.length)
|
|
|
|
|
, url
|
|
|
|
|
, linkHelper.findAcctColor(url)
|
|
|
|
|
, options.linkTag
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
2018-08-25 13:18:08 +02:00
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
TITLE(
|
|
|
|
|
allowInside = hashSetOf(
|
|
|
|
|
BIG,
|
|
|
|
|
BOLD,
|
|
|
|
|
MOTION,
|
|
|
|
|
URL,
|
|
|
|
|
LINK,
|
|
|
|
|
MENTION,
|
|
|
|
|
HASHTAG,
|
|
|
|
|
EMOJI,
|
|
|
|
|
CODE_INLINE
|
|
|
|
|
),
|
|
|
|
|
render = {
|
|
|
|
|
closePreviousBlock()
|
|
|
|
|
|
|
|
|
|
val start = this.start
|
|
|
|
|
fireRenderChildNodes(it) // 改行を含まないことが分かっている
|
|
|
|
|
spanList.addLast(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
android.text.style.AlignmentSpan.Standard(android.text.Layout.Alignment.ALIGN_CENTER)
|
|
|
|
|
)
|
|
|
|
|
spanList.addLast(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
android.text.style.BackgroundColorSpan(0x20808080)
|
|
|
|
|
)
|
|
|
|
|
spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(1.5f))
|
|
|
|
|
|
|
|
|
|
closeBlock()
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
QUOTE_BLOCK(
|
|
|
|
|
allowInsideAll = true,
|
|
|
|
|
render = {
|
|
|
|
|
closePreviousBlock()
|
|
|
|
|
|
|
|
|
|
val start = this.start
|
|
|
|
|
|
|
|
|
|
// 末尾にある空白のテキストノードを除去する
|
|
|
|
|
while(it.childNodes.isNotEmpty()) {
|
|
|
|
|
val last = it.childNodes.last()
|
|
|
|
|
if(last.type == Node2Type.TEXT && last.args[0].isBlank()) {
|
|
|
|
|
it.childNodes.removeLast()
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fireRenderChildNodes(it)
|
|
|
|
|
|
|
|
|
|
// TextView の文字装飾では「ブロック要素の入れ子」を表現できない
|
|
|
|
|
// 内容の各行の始端に何か追加するというのがまずキツい
|
|
|
|
|
// しかし各行の頭に引用マークをつけないと引用のネストで意味が通じなくなってしまう
|
|
|
|
|
val tmp = sb.toString()
|
|
|
|
|
//log.d("QUOTE_BLOCK tmp=${tmp} start=$start end=${tmp.length}")
|
|
|
|
|
for(i in tmp.length - 1 downTo start) {
|
|
|
|
|
val prevChar = when(i) {
|
|
|
|
|
start -> '\n'
|
|
|
|
|
else -> tmp[i - 1]
|
|
|
|
|
}
|
|
|
|
|
//log.d("QUOTE_BLOCK prevChar=${ String.format("%x",prevChar.toInt())}")
|
|
|
|
|
if(prevChar == '\n') {
|
|
|
|
|
//log.d("QUOTE_BLOCK insert! i=$i")
|
|
|
|
|
sb.insert(i, "> ")
|
|
|
|
|
spanList.insert(i, 2)
|
|
|
|
|
spanList.addLast(
|
|
|
|
|
i, i + 1,
|
|
|
|
|
android.text.style.BackgroundColorSpan(0x20808080)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spanList.addLast(
|
|
|
|
|
start,
|
|
|
|
|
sb.length,
|
|
|
|
|
CalligraphyTypefaceSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
closeBlock()
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
ROOT(
|
|
|
|
|
allowInsideAll = true,
|
|
|
|
|
render = { fireRenderChildNodes(it) }
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val nodeTypeAllSet = HashSet<Node2Type>().apply {
|
|
|
|
|
for(v in Node2Type.values()) {
|
|
|
|
|
this.add(v)
|
2018-08-25 13:18:08 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
class Node(
|
|
|
|
|
val type : Node2Type, // ノード種別
|
|
|
|
|
val args : Array<String> = emptyArray() // 引数
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
val childNodes = LinkedList<Node>()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class NodeDetected(
|
|
|
|
|
val node : Node,
|
|
|
|
|
val start : Int, // テキスト中の開始位置
|
|
|
|
|
val length : Int, // テキスト中の長さ
|
|
|
|
|
val textInside : String, // 内部範囲。親から継承する場合もあるし独自に作る場合もある
|
|
|
|
|
val startInside : Int, // 内部範囲の開始位置
|
|
|
|
|
private val lengthInside : Int // 内部範囲の終了位置
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
val end : Int
|
|
|
|
|
get() = start + length
|
|
|
|
|
|
|
|
|
|
val endInside : Int
|
|
|
|
|
get() = startInside + lengthInside
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class NodeParseEnv(
|
|
|
|
|
val parent : Node,
|
|
|
|
|
val text : String,
|
|
|
|
|
start : Int,
|
|
|
|
|
val end : Int
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
private val childNodes = parent.childNodes
|
|
|
|
|
private val allowInside = if(parent.type.allowInsideAll) {
|
|
|
|
|
nodeTypeAllSet
|
|
|
|
|
} else {
|
|
|
|
|
parent.type.allowInside
|
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
|
|
|
|
var remain : String = ""
|
|
|
|
|
var previous : String = ""
|
2018-08-24 14:37:47 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
private var lastEnd = start // 直前のノードの終了位置
|
2018-08-24 14:37:47 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
var pos : Int = start
|
2018-08-24 02:26:45 +02:00
|
|
|
|
set(value) {
|
|
|
|
|
field = value
|
2018-11-21 08:42:21 +01:00
|
|
|
|
remain = text.substring(pos, end)
|
2018-08-24 14:37:47 +02:00
|
|
|
|
previous = text.substring(lastEnd, pos)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
2018-08-24 14:37:47 +02:00
|
|
|
|
|
|
|
|
|
// 直前のノードの終了位置から次のノードの開始位置の手前までをresultに追加する
|
2018-11-21 08:42:21 +01:00
|
|
|
|
private fun closeText(endText : Int) {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val length = endText - lastEnd
|
2018-11-21 08:42:21 +01:00
|
|
|
|
if(length <= 0) return
|
|
|
|
|
val textInside = text.substring(lastEnd, endText)
|
|
|
|
|
childNodes.add(Node(Node2Type.TEXT, arrayOf(textInside)))
|
2018-08-24 14:37:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun parseInside() {
|
|
|
|
|
if(allowInside.isEmpty()) return
|
|
|
|
|
|
|
|
|
|
var i = lastEnd //スキャン中の位置
|
2018-08-24 14:37:47 +02:00
|
|
|
|
while(i < end) {
|
|
|
|
|
val lastParsers = nodeParserMap[text[i].toInt()]
|
|
|
|
|
if(lastParsers == null) {
|
|
|
|
|
++ i
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
pos = i
|
2018-11-21 08:42:21 +01:00
|
|
|
|
val detected = lastParsers.firstNonNull {
|
|
|
|
|
val d = this.it()
|
|
|
|
|
if(d == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
val n = d.node
|
|
|
|
|
if(! allowInside.contains(d.node.type)) {
|
|
|
|
|
log.w(
|
|
|
|
|
"not allowed : ${parent.type} => ${n.type} ${text.substring(
|
|
|
|
|
d.start,
|
|
|
|
|
d.end
|
|
|
|
|
)}"
|
|
|
|
|
)
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
d
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(detected == null) {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
++ i
|
|
|
|
|
continue
|
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
closeText(detected.start)
|
|
|
|
|
childNodes.add(detected.node)
|
|
|
|
|
i = detected.end
|
2018-08-24 14:37:47 +02:00
|
|
|
|
lastEnd = i
|
2018-11-21 08:42:21 +01:00
|
|
|
|
|
|
|
|
|
NodeParseEnv(
|
|
|
|
|
detected.node,
|
|
|
|
|
detected.textInside,
|
|
|
|
|
detected.startInside,
|
|
|
|
|
detected.endInside
|
|
|
|
|
).parseInside()
|
2018-08-24 14:37:47 +02:00
|
|
|
|
}
|
2018-11-21 08:42:21 +01:00
|
|
|
|
closeText(end)
|
2018-08-24 14:37:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun makeDetected(
|
|
|
|
|
node : Node,
|
|
|
|
|
start : Int,
|
|
|
|
|
length : Int,
|
|
|
|
|
textInside : String,
|
|
|
|
|
startInside : Int,
|
|
|
|
|
lengthInside : Int
|
|
|
|
|
) : NodeDetected {
|
|
|
|
|
|
|
|
|
|
if(DEBUG) log.d(
|
|
|
|
|
"NodeDetected: ${node.type} inside=${
|
|
|
|
|
textInside.substring(startInside, startInside + lengthInside)
|
|
|
|
|
}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return NodeDetected(
|
|
|
|
|
node,
|
|
|
|
|
pos + start,
|
|
|
|
|
length,
|
|
|
|
|
textInside,
|
|
|
|
|
startInside,
|
|
|
|
|
lengthInside
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// ノードのパースを行う関数をキャプチャパラメータつきで生成する
|
2018-08-24 02:26:45 +02:00
|
|
|
|
private fun simpleParser(
|
|
|
|
|
pattern : Pattern
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, type : Node2Type
|
|
|
|
|
) : NodeParseEnv.() -> NodeDetected? = {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val matcher = pattern.matcher(remain)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
when {
|
|
|
|
|
! matcher.find() -> null
|
2018-11-21 08:42:21 +01:00
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
|
|
|
|
val textInside = matcher.group(1)
|
|
|
|
|
makeDetected(
|
|
|
|
|
Node(type, arrayOf(textInside)),
|
|
|
|
|
0, matcher.end(),
|
|
|
|
|
this.text, pos + matcher.start(1), textInside.length
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// (マークダウン要素の特徴的な文字)と(パーサ関数の配列)のマップ
|
2018-11-21 08:42:21 +01:00
|
|
|
|
private val nodeParserMap = SparseArray<Array<out NodeParseEnv.() -> NodeDetected?>>().apply {
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
fun addParser(firstChars : String, vararg nodeParsers : NodeParseEnv.() -> NodeDetected?) {
|
2018-08-24 02:26:45 +02:00
|
|
|
|
for(s in firstChars) {
|
|
|
|
|
put(s.toInt(), nodeParsers)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
// Quote "..."
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
"\""
|
2018-08-25 13:18:08 +02:00
|
|
|
|
, simpleParser(
|
2018-11-21 08:42:21 +01:00
|
|
|
|
Pattern.compile("""^"([^\x0d\x0a]+?)\n"[\x0d\x0a]*""")
|
|
|
|
|
, Node2Type.QUOTE_INLINE
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
// Quote (行頭)>...(改行)
|
|
|
|
|
val reQuoteBlock = Pattern.compile(
|
|
|
|
|
"^>(?:[ ]?)([^\\x0d\\x0a]*)(\\x0a|\\x0d\\x0a?)?",
|
|
|
|
|
Pattern.MULTILINE
|
|
|
|
|
)
|
|
|
|
|
addParser(">", {
|
|
|
|
|
if(pos > 0) {
|
|
|
|
|
val c = text[pos - 1]
|
|
|
|
|
if(c != '\r' && c != '\n') {
|
|
|
|
|
//直前が改行文字ではない
|
|
|
|
|
if(DEBUG) log.d("QUOTE: previous char is not line end. ${c} pos=$pos text=$text")
|
|
|
|
|
return@addParser null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var p = 0
|
|
|
|
|
val content = StringBuilder()
|
|
|
|
|
val matcher = reQuoteBlock.matcher(remain)
|
|
|
|
|
while(true) {
|
|
|
|
|
if(! matcher.find(p)) break
|
|
|
|
|
p = matcher.end()
|
|
|
|
|
if(content.isNotEmpty()) content.append('\n')
|
|
|
|
|
content.append(matcher.group(1))
|
|
|
|
|
}
|
|
|
|
|
if(content.isNotEmpty()) content.append('\n')
|
|
|
|
|
|
|
|
|
|
if(p == 0) {
|
|
|
|
|
// > のあとに全く何もない
|
|
|
|
|
if(DEBUG) log.d("QUOTE: not a quote")
|
|
|
|
|
return@addParser null
|
|
|
|
|
}
|
|
|
|
|
val textInside = content.toString()
|
|
|
|
|
|
|
|
|
|
makeDetected(
|
|
|
|
|
Node(Node2Type.QUOTE_BLOCK),
|
|
|
|
|
0, p,
|
|
|
|
|
textInside, 0, textInside.length
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// 絵文字 :emoji:
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
":"
|
|
|
|
|
, simpleParser(
|
|
|
|
|
Pattern.compile("""^:([a-zA-Z0-9+-_]+):""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.EMOJI
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
"("
|
|
|
|
|
, simpleParser(
|
|
|
|
|
Pattern.compile("""^\Q(((\E(.+?)\Q)))\E""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.MOTION
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
"<"
|
|
|
|
|
, simpleParser(
|
|
|
|
|
Pattern.compile("""^<motion>(.+?)</motion>""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.MOTION
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// ***big*** **bold**
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
"*"
|
|
|
|
|
// 処理順序に意味があるので入れ替えないこと
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// 記号列が長い順にパースを試す
|
2018-08-24 02:26:45 +02:00
|
|
|
|
, simpleParser(
|
|
|
|
|
Pattern.compile("""^\Q***\E(.+?)\Q***\E""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.BIG
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
, simpleParser(
|
|
|
|
|
Pattern.compile("""^\Q**\E(.+?)\Q**\E""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.BOLD
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
2018-08-22 15:06:58 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// http(s)://....
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
"h"
|
|
|
|
|
, simpleParser(
|
|
|
|
|
Pattern.compile("""^(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.URL
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// 検索
|
|
|
|
|
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val reSearchButton = Pattern.compile(
|
|
|
|
|
"""^(検索|\[検索]|Search|\[Search])(\n|${'$'})"""
|
|
|
|
|
, Pattern.CASE_INSENSITIVE
|
|
|
|
|
)
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-24 02:26:45 +02:00
|
|
|
|
fun parseSearchPrev(prev : String) : String? {
|
|
|
|
|
val delm = prev.lastIndexOf('\n')
|
|
|
|
|
val end = prev.length
|
|
|
|
|
return when {
|
|
|
|
|
end <= 1 -> null // キーワードを含まないくらい短い
|
|
|
|
|
delm + 1 >= end - 1 -> null // 改行より後の部分が短すぎる
|
|
|
|
|
! " ".contains(prev.last()) -> null // 末尾が空白ではない
|
|
|
|
|
else -> prev.substring(delm + 1, end - 1) // キーワード部分を返す
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
val searchParser : NodeParseEnv.() -> NodeDetected? = {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val matcher = reSearchButton.matcher(remain)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
when {
|
|
|
|
|
! matcher.find() -> null
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
val buttonLength = matcher.end()
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val keyword = parseSearchPrev(previous)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
when {
|
|
|
|
|
keyword?.isEmpty() != false -> null
|
2018-11-21 08:42:21 +01:00
|
|
|
|
|
|
|
|
|
else -> makeDetected(
|
|
|
|
|
Node(Node2Type.SEARCH, arrayOf(keyword)),
|
|
|
|
|
- (keyword.length + 1),
|
|
|
|
|
buttonLength + (keyword.length + 1),
|
|
|
|
|
this.text, pos - (keyword.length + 1), keyword.length
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
// [title] 【title】
|
|
|
|
|
// 直後に改行が必要だったが文末でも良いことになった https://github.com/syuilo/misskey/commit/79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae
|
2018-08-24 07:04:55 +02:00
|
|
|
|
val titleParser = simpleParser(
|
2018-11-21 08:42:21 +01:00
|
|
|
|
Pattern.compile("""^[【\[](.+?)[】\]](\n|\z)""")
|
|
|
|
|
, Node2Type.TITLE
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// Link
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val reLink = Pattern.compile(
|
|
|
|
|
"""^\??\[([^\[\]]+?)]\((https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+?)\)"""
|
|
|
|
|
)
|
2018-08-24 07:04:55 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
val linkParser : NodeParseEnv.() -> NodeDetected? = {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val matcher = reLink.matcher(remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 02:26:45 +02:00
|
|
|
|
! matcher.find() -> null
|
2018-11-21 08:42:21 +01:00
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
val title = matcher.group(1)
|
|
|
|
|
makeDetected(
|
|
|
|
|
Node(
|
|
|
|
|
Node2Type.LINK,
|
|
|
|
|
arrayOf(
|
|
|
|
|
title
|
|
|
|
|
, matcher.group(2) // url
|
|
|
|
|
, remain[0].toString() // silent なら "?" になる
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
0, matcher.end(),
|
|
|
|
|
this.text, pos + matcher.start(1), title.length
|
2018-08-22 07:11:25 +02:00
|
|
|
|
)
|
2018-11-21 08:42:21 +01:00
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// [ はいろんな要素で使われる
|
2018-11-21 08:42:21 +01:00
|
|
|
|
// searchの判定をtitleより前に行うこと。 「abc [検索] 」でtitleが優先されるとマズい
|
|
|
|
|
addParser("[", searchParser, titleParser, linkParser)
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// その他の文字でも判定する
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser("【", titleParser)
|
|
|
|
|
addParser("検Ss", searchParser)
|
|
|
|
|
addParser("?", linkParser)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// メンション @username @username@host
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val reMention = Pattern.compile(
|
|
|
|
|
"""^@([a-z0-9_]+)(?:@([a-z0-9.\-]+[a-z0-9]))?"""
|
|
|
|
|
, Pattern.CASE_INSENSITIVE
|
|
|
|
|
)
|
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
addParser("@", {
|
|
|
|
|
val matcher = reMention.matcher(remain)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
when {
|
2018-08-24 02:26:45 +02:00
|
|
|
|
! matcher.find() -> null
|
2018-11-21 08:42:21 +01:00
|
|
|
|
else -> makeDetected(
|
|
|
|
|
Node(
|
|
|
|
|
Node2Type.MENTION,
|
|
|
|
|
arrayOf(
|
|
|
|
|
matcher.group(1),
|
|
|
|
|
matcher.group(2) ?: "" // username, host
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
0, matcher.end(),
|
|
|
|
|
"", 0, 0
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
})
|
2018-08-25 13:18:08 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// Hashtag
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val reHashtag = Pattern.compile("""^#([^\s]+)""")
|
|
|
|
|
addParser("#"
|
2018-08-24 14:37:47 +02:00
|
|
|
|
, {
|
|
|
|
|
val matcher = reHashtag.matcher(remain)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
when {
|
|
|
|
|
! matcher.find() -> null
|
|
|
|
|
else -> when {
|
|
|
|
|
// 先頭以外では直前に空白が必要らしい
|
2018-11-21 08:42:21 +01:00
|
|
|
|
pos > 0 &&
|
|
|
|
|
! CharacterGroup.isWhitespace(text[pos - 1].toInt()) ->
|
2018-08-24 02:26:45 +02:00
|
|
|
|
null
|
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
else -> makeDetected(
|
|
|
|
|
Node(
|
|
|
|
|
Node2Type.HASHTAG,
|
|
|
|
|
arrayOf(matcher.group(1)) // 先頭の#を含まない
|
|
|
|
|
),
|
|
|
|
|
0, matcher.end(),
|
|
|
|
|
"", 0, 0
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
}
|
2018-08-22 07:11:25 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
2018-08-25 13:18:08 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// code (ブロック、インライン)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
addParser(
|
|
|
|
|
"`"
|
|
|
|
|
, simpleParser(
|
2018-11-21 08:42:21 +01:00
|
|
|
|
Pattern.compile("""^```(.+?)```[\x0d\x0a]*""", Pattern.DOTALL)
|
|
|
|
|
, Node2Type.CODE_BLOCK
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
, simpleParser(
|
|
|
|
|
// インラインコードは内部にとある文字を含むと認識されない。理由は顔文字と衝突するからだとか
|
2018-08-24 07:04:55 +02:00
|
|
|
|
Pattern.compile("""^`([^`´\x0d\x0a]+)`""")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
, Node2Type.CODE_INLINE
|
2018-08-25 13:18:08 +02:00
|
|
|
|
)
|
2018-08-24 02:26:45 +02:00
|
|
|
|
)
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
|
2018-08-24 14:37:47 +02:00
|
|
|
|
// このファイルのエントリーポイント
|
2018-08-24 02:26:45 +02:00
|
|
|
|
fun decodeMarkdown(options : DecodeOptions, src : String?) =
|
|
|
|
|
SpannableStringBuilderEx().apply {
|
2018-11-04 03:40:28 +01:00
|
|
|
|
val save = options.enlargeCustomEmoji
|
|
|
|
|
options.enlargeCustomEmoji = 2.5f
|
2018-08-22 01:35:54 +02:00
|
|
|
|
try {
|
2018-08-24 14:37:47 +02:00
|
|
|
|
val env = SpanOutputEnv(options, this)
|
2018-08-22 07:11:25 +02:00
|
|
|
|
|
2018-11-21 08:42:21 +01:00
|
|
|
|
if(src != null) {
|
|
|
|
|
val root = Node(Node2Type.ROOT)
|
|
|
|
|
NodeParseEnv(root, src, 0, src.length).parseInside()
|
|
|
|
|
for(sp in env.fireRender(root).list) {
|
|
|
|
|
env.sb.setSpan(sp.span, sp.start, sp.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
|
|
|
}
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 末尾の空白を取り除く
|
2018-08-24 02:26:45 +02:00
|
|
|
|
val end = length
|
|
|
|
|
var pos = end
|
|
|
|
|
while(pos > 0 && HTMLDecoder.isWhitespaceOrLineFeed(get(pos - 1).toInt())) -- pos
|
|
|
|
|
if(pos < end) delete(pos, end)
|
|
|
|
|
|
|
|
|
|
} catch(ex : Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
log.e(ex, "decodeMarkdown failed")
|
2018-11-21 08:42:21 +01:00
|
|
|
|
} finally {
|
2018-11-04 03:40:28 +01:00
|
|
|
|
options.enlargeCustomEmoji = save
|
2018-08-22 01:35:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|