Misskey Markdownのデコーダを書いた

This commit is contained in:
tateisu 2018-08-22 08:35:54 +09:00
parent 8f119b5c28
commit 605afa9323
9 changed files with 1006 additions and 27 deletions

View File

@ -15,6 +15,7 @@
<inspection_tool class="LocalVariableName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="LoopToCallChain" enabled="false" level="INFO" enabled_by_default="false" />
<inspection_tool class="NumericOverflow" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ObjectPropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PrivatePropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="PropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RemoveCurlyBracesFromTemplate" enabled="false" level="WEAK WARNING" enabled_by_default="false" />

View File

@ -107,7 +107,8 @@ class ActMain : AppCompatActivity()
val reStatusPage = Pattern.compile("\\Ahttps://([^/]+)/@([A-Za-z0-9_]+)/(\\d+)(?:\\z|[?#])")
var boostButtonSize = 0
var timeline_font : Typeface = Typeface.DEFAULT
var timeline_font_bold : Typeface = Typeface.DEFAULT_BOLD
}
// @Override
@ -151,8 +152,7 @@ class ActMain : AppCompatActivity()
val viewPool = RecyclerView.RecycledViewPool()
var timeline_font : Typeface? = null
var timeline_font_bold : Typeface? = null
var avatarIconSize : Int = 0
var notificationTlIconSize : Int = 0

View File

@ -757,38 +757,32 @@ class Column(
}
private fun getNotificationTypeString() : String {
var n = 0
val sb = StringBuilder()
sb.append("(")
var n = 0
if(! dont_show_reply) {
++n
if(sb.isNotEmpty()) sb.append(", ")
if(n++>0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_mention))
}
if(! dont_show_follow) {
++n
if(sb.isNotEmpty()) sb.append(", ")
if(n++>0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_follow))
}
if(! dont_show_boost) {
++n
if(sb.isNotEmpty()) sb.append(", ")
if(n++>0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_boost))
}
if(! dont_show_favourite) {
++n
if(sb.isNotEmpty()) sb.append(", ")
if(n++>0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_favourite))
}
if(! dont_show_reaction) {
++n
if(sb.isNotEmpty()) sb.append(", ")
if(n++>0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_reaction))
}
if(! dont_show_vote) {
++n
if(sb.isNotEmpty()) sb.append(", ")
if(n++>0) sb.append(", ")
sb.append(context.getString(R.string.notification_type_vote))
}
if( n == 0 || n == 6 ) return "" // 全部か皆無なら部分表記は要らない

View File

@ -163,13 +163,13 @@ class ColumnViewHolder(
init {
if(activity.timeline_font != null) {
if(ActMain.timeline_font != null) {
viewRoot.scan { v ->
try {
if(v is Button) {
// ボタンは触らない
} else if(v is TextView) {
v.typeface = activity.timeline_font
v.typeface = ActMain.timeline_font
}
} catch(ex : Throwable) {
log.trace(ex)

View File

@ -242,9 +242,9 @@ internal class ItemViewHolder(
this.access_info = column.access_info
if(activity.timeline_font != null || activity.timeline_font_bold != null) {
val font_bold = activity.timeline_font_bold ?: activity.timeline_font
val font_normal = activity.timeline_font ?: activity.timeline_font_bold
if(ActMain.timeline_font != null || ActMain.timeline_font_bold != null) {
val font_bold = ActMain.timeline_font_bold ?: ActMain.timeline_font
val font_normal = ActMain.timeline_font ?: ActMain.timeline_font_bold
viewRoot.scan { v ->
try {
if(v is CountImageButton) {

View File

@ -24,8 +24,8 @@ internal abstract class ViewHolderHeaderBase(val activity : ActMain, val viewRoo
if(v is Button) {
// ボタンは太字なので触らない
} else if(v is TextView) {
if(activity.timeline_font != null) {
v.typeface = activity.timeline_font
if(ActMain.timeline_font != null) {
v.typeface = ActMain.timeline_font
}
if(! activity.timeline_font_size_sp.isNaN()) {
v.textSize = activity.timeline_font_size_sp

View File

@ -262,8 +262,8 @@ internal class ViewHolderHeaderProfile(
val content_color = column.content_color
val c = if(content_color != 0) content_color else default_color
val nameTypeface = activity.timeline_font_bold ?: Typeface.DEFAULT_BOLD
val valueTypeface = activity.timeline_font ?: Typeface.DEFAULT
val nameTypeface = ActMain.timeline_font_bold ?: Typeface.DEFAULT_BOLD
val valueTypeface = ActMain.timeline_font ?: Typeface.DEFAULT
for(item in who.fields) {

View File

@ -52,7 +52,7 @@ object HTMLDecoder {
set
}
private fun isWhitespaceOrLineFeed(codepoint : Int) : Boolean {
fun isWhitespaceOrLineFeed(codepoint : Int) : Boolean {
return CharacterGroup.isWhitespace(codepoint) || when(codepoint) {
0x0a, 0x0d -> true
else -> false
@ -470,6 +470,10 @@ object HTMLDecoder {
fun decodeHTML(options : DecodeOptions, src : String?) : SpannableStringBuilder {
if( options.linkHelper?.isMisskey == true){
return MisskeyMarkdownDecoder.decodeMarkdown(options,src)
}
val sb = SpannableStringBuilder()
try {

View File

@ -0,0 +1,980 @@
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
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.span.EmojiImageSpan
import jp.juggler.subwaytooter.span.HighlightSpan
import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.table.HighlightWord
import uk.co.chrisjenx.calligraphy.CalligraphyTypefaceSpan
import java.util.regex.Pattern
private fun String.safeSubstring(end:Int):String{
val l = this.length
if( end > l ) return this
return this.substring(0,end)
}
object MisskeySyntaxHighlighter {
private val symbols = setOf(
"=",
"+",
"-",
"*",
"/",
"%",
"~",
"^",
"&",
"|",
">",
"<",
"!",
"?"
)
// 文字数が多い順にソートします
// そうしないと、「function」という文字列が与えられたときに「func」が先にマッチしてしまう可能性があるためです
private 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"
)
private val keywords = ArrayList<String>().apply {
// lower
addAll(_keywords)
// UPPER
addAll(_keywords.map { k -> k.toUpperCase() })
// Snake
addAll(_keywords.map { k -> k[0].toUpperCase() + k.substring(1) })
// 長い順にソート
sortWith(Comparator { a, b -> b.length - a.length })
}
private class Token(
var length : Int,
var color:Int =0,
val italic :Boolean = false
)
private class Env(
var source:String,
var pos:Int,
var remain:String
)
private val reLineComment = Pattern.compile("^//(.+?)(\n|$)")
private val reBlockComment = Pattern.compile("""^/\*([\s\S]*?)\*/""")
private val reStringStart = Pattern.compile("""^(["`])""")
private val reLabel = Pattern.compile("""^@([a-zA-Z_-]+?)\n""")
private val reAlphabet = Pattern.compile("""[a-zA-Z]""")
private val reNumber = Pattern.compile("""^[+-]?[\d.]+""")
private val reMethod = Pattern.compile("""^([a-zA-Z_-]+?)\(""")
private val reProperty = Pattern.compile("""^[a-zA-Z0-9_-]+""")
private val reStartAlphabet = Pattern.compile("""^[a-zA-Z]""")
private val elements = arrayOf(
// comment
{ env:Env ->
if( env.remain.safeSubstring( 2) != "//") return@arrayOf null
val match = reLineComment.matcher(env.remain)
if(! match.find()) return@arrayOf null
val comment = match.group()
Token(
color=0x7f00000,
length = comment.length
)
},
// block comment
{ env:Env ->
val match = reBlockComment.matcher(env.remain)
if(! match.find()) return@arrayOf null
val g0 = match.group()
Token(length = g0.length,color=0x7f00000)
},
// string
{ env:Env ->
val match = reStringStart.matcher(env.remain)
if(! match.find()) return@arrayOf null
val begin = env.remain[0]
val str = StringBuilder().append(begin)
var thisIsNotAString = false
var i = 1
loop@ while(i < env.remain.length) {
val char = env.remain[i ++]
when {
char == '\\' -> {
str.append(char)
if(i < env.remain.length) str.append(env.remain[i ++])
continue@loop
}
char == begin -> {
str.append(char)
break@loop
}
char == '\n' || i >= env.remain.length -> {
thisIsNotAString = true
break@loop
}
else -> str.append(char)
}
}
if(thisIsNotAString) {
null
} else {
Token(length = str.length,color = 0xe96900)
}
},
// regexp
{ env:Env ->
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 ++]
if(char == '\\') {
regexp.append(char)
if(i < env.remain.length) regexp.append(env.remain[i ++])
continue
} else if(char == '/') {
break
} else if(char == '\n' || i >= env.remain.length) {
thisIsNotARegexp = true
break
} else {
regexp.append(char)
}
}
if(thisIsNotARegexp) {
null
} else if(regexp.isEmpty()) {
null
} else if(regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') {
null
} else {
Token(length = regexp.length + 2,color=0xe9003f)
}
},
// label
{ env:Env ->
if(env.remain[0] != '@') return@arrayOf null
val match = reLabel.matcher(env.remain)
if(! match.find()) return@arrayOf null
val label = match.group(0)
Token(length = label.length,color=0xe9003f)
},
// number
{ env:Env ->
val prev = if(env.pos <= 0) null else env.source[env.pos - 1].toString()
if(prev != null && reAlphabet.matcher(prev).find()) return@arrayOf null
val match = reNumber.matcher(env.remain)
if(match.find()) {
val g0 = match.group(0)
Token(length = g0.length,color=0xae81ff)
} else {
null
}
},
// nan
{ env:Env ->
val prev = if(env.pos <= 0) null else env.source[env.pos - 1].toString()
if(prev != null && reAlphabet.matcher(prev).find()) return@arrayOf null
if(env.remain.safeSubstring( 3) == "NaN") {
Token(length = 3,color=0xae81ff)
} else {
null
}
},
// method
{ env:Env ->
val match = reMethod.matcher(env.remain)
if(match.find()) {
val g1 = match.group(1)
if(g1 != "-") {
return@arrayOf Token(length = g1.length,color=0x8964c1,italic = true)
}
}
null
},
// property
{ env:Env ->
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
if(prev != '.') return@arrayOf null
val match = reProperty.matcher(env.remain)
if(match.find()) {
val g0 = match.group()
return@arrayOf Token(length = g0.length,color=0xa71d5d)
}
null
},
// keyword
{ env:Env ->
val prev = if(env.pos <= 0) "" else env.source[env.pos - 1].toString()
if(reAlphabet.matcher(prev).find()) return@arrayOf null
val match = keywords.find {
env.remain.safeSubstring(it.length) == it
}
?: return@arrayOf null
val kw = env.remain.safeSubstring( match.length)
// 先頭は英字
if( reStartAlphabet.matcher(kw).find() ){
return@arrayOf when( kw ){
"true","false","null","nil","undefined" ->
Token( length = kw.length, color = 0xae81ff)
else->
Token( length = kw.length, color = 0x2973b7)
}
}
null
},
// symbol
{ env:Env ->
val s = env.remain[0].toString()
if(symbols.contains(s)) {
return@arrayOf Token(length=1,color=0x42b983)
}
null
}
)
fun parse(source : String) : SpannableStringBuilder {
val sb =SpannableStringBuilder()
val env = Env(source=source,pos=0,remain=source)
fun push(pos:Int, token : Token) {
val start = pos
val end = pos+ token.length
sb.append( source.substring(start,end))
env.pos = end
var c = token.color
if( c !=0){
if( c <0x1000000) {
c = c or Color.BLACK
}
sb.setSpan(ForegroundColorSpan( c)
,start,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
if( token.italic){
sb.setSpan(CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC))
,start,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
var textToken : Token? = null
var textTokenStart = 0
fun closeTextToken(){
val token = textToken
if(token != null){
token.length = env.pos -textTokenStart
push(textTokenStart,token)
textToken = null
}
}
loop1@ while(env.remain.isNotEmpty()) {
for(el in elements) {
val token = el(env) ?: continue
closeTextToken()
push(env.pos,token)
env.remain = source.substring(env.pos)
continue@loop1
}
if( textToken == null ){
textToken = Token(length = 0)
textTokenStart = env.pos
}
env.remain = source.substring(++env.pos)
}
closeTextToken()
return sb
}
}
enum class NodeType {
Text,
Big,
Bold,
Title,
Url,
Link,
Mention,
Hashtag,
CodeBlock,
CodeInline,
Quote,
Emoji,
Search,
Motion
}
private class Node(
var type : NodeType,
var sourceStart : Int,
var sourceLength : Int,
var data : ArrayList<String?>?
)
private class ParserEnv(
val text : String
, var pos : Int
, var remain : String
)
// 指定された位置から始まるノードがあれば処理してノードのリストを返す。
// なければ偽を返す
private typealias NodeParser = (env : ParserEnv) -> List<Node>?
object MisskeyMarkdownDecoder {
private val log = LogCategory("MisskeyMarkdownDecoder")
//////////////////////////////////////
// parser
private fun ParserEnv.genNode1(type : NodeType, sourceLength : Int, data : ArrayList<String?>?) : List<Node> {
return listOf(
Node(
type = type,
sourceStart = pos,
sourceLength = sourceLength,
data = data
)
)
}
private fun generateSimpleNodeParser(
type : NodeType,
pattern : Pattern
) : NodeParser = { env : ParserEnv ->
val matcher = pattern.matcher(env.remain)
when {
matcher.find() -> env.genNode1(
type
, matcher.end()
, arrayListOf(matcher.group(1))
)
else -> null
}
}
private fun generateLinkParser(
type : NodeType,
pattern : Pattern
) : NodeParser = { env : ParserEnv ->
val matcher = pattern.matcher(env.remain)
when {
matcher.find() -> env.genNode1(
type
, matcher.end()
, arrayListOf(
matcher.group(1) // title
, matcher.group(2) // url
, env.remain[0].toString() // silent なら "?" になる
)
)
else -> null
}
}
private fun generateMentionParser(
type : NodeType,
pattern : Pattern
) : NodeParser = { env : ParserEnv ->
val matcher = pattern.matcher(env.remain)
when {
matcher.find() -> env.genNode1(
type
, matcher.end()
, arrayListOf(
matcher.group(1) // username
, matcher.group(2) // host
)
)
else -> null
}
}
private fun generateHashtagParser(
type : NodeType,
pattern : Pattern
) : NodeParser = { env : ParserEnv ->
val matcher = pattern.matcher(env.remain)
when {
matcher.find() -> when {
// 先頭以外では直前に空白が必要らしい
env.pos > 0 && ! CharacterGroup.isWhitespace(
env.text[env.pos - 1].toInt()
) -> null
else -> env.genNode1(
type
, matcher.end()
, arrayListOf(
matcher.group(1) // 先頭の#を含まないハッシュタグ
)
)
}
else -> null
}
}
private fun generateCodeInlineParser(
type : NodeType,
pattern : Pattern
) : NodeParser = { env : ParserEnv ->
val matcher = pattern.matcher(env.remain)
when {
matcher.find() ->when{
// インラインコードは内部にある文字を含むと認識されない。理由は謎
matcher.group(1).contains('´') -> null
else->env.genNode1(
type
, matcher.end()
, arrayListOf(
matcher.group(1)
)
)
}
else -> null
}
}
private val reMotion1 = Pattern.compile("""^\Q(((\E(.+?)\Q)))\E""")
private val reMotion2 = Pattern.compile("""^<motion>(.+?)</motion>""")
private fun generateMotionNodeParser(type : NodeType) : NodeParser {
return { 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(
type
, matcher.end()
, arrayListOf(
matcher.group(1) // 先頭の#を含まないハッシュタグ
)
)
else -> null
}
}
}
private val nodeParserList = arrayOf(
// 処理順序に意味があるので入れ替えないこと
// 記号列が長い順
generateSimpleNodeParser(
NodeType.Big,
Pattern.compile("""^\Q***\E(.+?)\Q***\E""")
),
generateSimpleNodeParser(
NodeType.Bold,
Pattern.compile("""^\Q**\E(.+?)\Q**\E""")
),
generateSimpleNodeParser(
NodeType.Title,
Pattern.compile("""^[【\[](.+?)[】\]]\n""")
),
generateSimpleNodeParser(
NodeType.Url,
Pattern.compile("""^(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
),
generateLinkParser(
NodeType.Link,
Pattern.compile("""^\??\[([^\[\]]+?)]\((https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+?)\)""")
),
generateMentionParser(
NodeType.Mention,
Pattern.compile(
"""^@([a-z0-9_]+)(?:@([a-z0-9.\-]+[a-z0-9]))?""",
Pattern.CASE_INSENSITIVE
)
),
generateHashtagParser(
NodeType.Hashtag,
Pattern.compile("""^#([^\s]+)""")
),
generateSimpleNodeParser(
NodeType.CodeBlock,
Pattern.compile("""^```([\s\S]+?)```""")
),
generateCodeInlineParser(
NodeType.CodeInline,
Pattern.compile("""^`(.+?)`""")
),
generateSimpleNodeParser(
NodeType.Quote,
Pattern.compile("""^"([\s\S]+?)\n"""")
),
generateSimpleNodeParser(
NodeType.Emoji,
Pattern.compile("""^:([a-zA-Z0-9+-_]+):""")
),
generateSimpleNodeParser(
NodeType.Search,
Pattern.compile(
"""^(.+?)[  ](検索|\[検索]|Search|\[Search])(\n|${'$'})""",
Pattern.CASE_INSENSITIVE
)
),
generateMotionNodeParser(NodeType.Motion)
)
private fun parse(source : String?) : ArrayList<Node> {
val result = ArrayList<Node>()
if(source!=null) {
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) {
val list = el(env) ?: continue
closeTextNode()
for(node in list) {
result.add(node)
env.pos += node.sourceLength
}
env.remain = env.text.substring(env.pos)
continue@loop1
}
// テキストノードの開始
if(textNode == null) {
textNode = Node(
NodeType.Text,
env.pos,
0,
null
)
}
env.remain = env.text.substring( ++ env.pos)
}
closeTextNode()
}
return result
}
fun decodeMarkdown(options : DecodeOptions, src : String?) : SpannableStringBuilder {
val sb = SpannableStringBuilder()
val context= options.context ?: return sb
fun urlShorter(display_url:String,href:String):CharSequence{
if( options.isMediaAttachment(href)) {
@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)
@Suppress("NAME_SHADOWING")
val sb = StringBuilder()
if(! display_url.startsWith("http")){
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) {
sb.append(remain.safeSubstring( 10))
sb.append("")
} else {
sb.append(remain)
}
return sb
} catch(ex : Throwable) {
log.trace(ex)
return display_url
}
}
try {
if(src != null) {
val font_bold = ActMain.timeline_font_bold ?: ActMain.timeline_font
for( node in parse(src) ){
val nodeSource = src.substring(node.sourceStart,node.sourceStart+node.sourceLength)
var start = sb.length
val data = node.data
fun setSpan(span:Any){
val end = sb.length
sb.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
fun setHighlight(){
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
}
}
}
}
}
fun appendText(text:CharSequence? ,preventHighlight:Boolean=false){
text?:return
sb.append(text)
if(!preventHighlight){
setHighlight()
}
}
fun appendTextCode(text:String? ,preventHighlight:Boolean=false){
text?:return
sb.append(MisskeySyntaxHighlighter.parse(text))
if(!preventHighlight){
setHighlight()
}
}
fun appendLink(text:String, url:String,allowShort:Boolean = false){
when {
text.isEmpty() -> return
!allowShort -> appendText(text,preventHighlight = true)
else -> {
val short = urlShorter(text,url)
appendText(short,preventHighlight = true)
}
}
val linkHelper = options.linkHelper
if(linkHelper != null) {
setSpan(MyClickableSpan(
text,
url,
linkHelper.findAcctColor(url), // TODO 通称と色 が働くか確認する
options.linkTag
))
}
// リンクスパンを設定した後に色をつける
setHighlight()
}
when(node.type){
NodeType.Url->{
val url = data?.get(0)
if(url?.isNotEmpty()==true){
appendLink(url,url,allowShort = true)
}
}
NodeType.Link ->{
val title = data?.get(0)?:"?"
val url = data?.get(1)
// val silent = data?.get(2)
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
if(url?.isNotEmpty()==true){
appendLink(title,url)
}
}
NodeType.Mention->{
val username = data?.get(0)?:""
val host = data?.get(1)?:""
val linkHelper = options.linkHelper
if(linkHelper == null) {
appendText(
if(host.isEmpty()) {
"@$username"
} else {
"@$username@$host"
}
)
}else{
val acct = if( Pref.bpMentionFullAcct(App1.pref)) {
when {
host.isEmpty() -> "@$username@${linkHelper.host}"
else -> "@$username@$host"
}
}else {
when {
host.isEmpty() -> "@$username"
host.equals(linkHelper.host,ignoreCase = true) -> "@$username"
else -> "@$username@$host"
}
}
appendLink(acct,"https://${linkHelper}/$acct")
}
}
NodeType.Hashtag ->{
val tag = data?.get(0)
if(tag?.isNotEmpty()==true){
appendLink("#$tag","https://misskey.m544.net/tags/"+tag.encodePercent())
}
}
NodeType.Text->{
appendText(nodeSource)
}
NodeType.Big ->{
appendText(data?.get(0))
setSpan(RelativeSizeSpan(1.5f))
setSpan(CalligraphyTypefaceSpan(font_bold))
// TODO アニメーション
}
NodeType.Bold->{
appendText(data?.get(0))
setSpan(CalligraphyTypefaceSpan(font_bold))
}
NodeType.Title->{
appendText(data?.get(0)?.trim{it<=' '})
setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER))
// TODO 見出し背景に色をつける
// ブロック要素なので改行必須
appendText("\n")
}
NodeType.CodeBlock->{
appendTextCode(data?.get(0)?.trimEnd())
setSpan(BackgroundColorSpan(0x40808080))
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
// ブロック要素なので改行必須
appendText("\n")
// TODO Syntax highlight
}
NodeType.CodeInline->{
appendTextCode(data?.get(0))
setSpan(BackgroundColorSpan(0x40808080))
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
// TODO Syntax highlight
}
NodeType.Quote->{
appendText(data?.get(0)?.trim())
setSpan(CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC)))
// ブロック要素なので改行必須
appendText("\n")
}
NodeType.Emoji->{
val code = data?.get(0)
if( code?.isNotEmpty()==true){
appendText(options.decodeEmoji(":$code:"))
}
}
NodeType.Search->{
val text = data?.get(0)
if( text?.isNotEmpty()==true){
appendText(text)
start = sb.length
appendLink(
context.getString(R.string.search),
"https://www.google.co.jp/search?q="+text.encodePercent()
)
// ブロック要素なので改行必須
appendText("\n")
}
}
NodeType.Motion->{
val code = data?.get(0)
appendText(code)
// TODO なんかする
}
}
}
// 末尾の空白を取り除く
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
}
}