(Misskey)MFMのcenterマークダウンに対応
This commit is contained in:
parent
32526a0bb8
commit
4fef191a35
|
@ -8,6 +8,7 @@ import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.BackgroundColorSpan
|
import android.text.style.BackgroundColorSpan
|
||||||
import android.text.style.ForegroundColorSpan
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.RelativeSizeSpan
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import android.util.SparseBooleanArray
|
import android.util.SparseBooleanArray
|
||||||
import jp.juggler.subwaytooter.ActMain
|
import jp.juggler.subwaytooter.ActMain
|
||||||
|
@ -38,16 +39,21 @@ private inline fun <T, V> Array<out T>.firstNonNull(predicate : (T) -> V?) : V?
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
class SpanPos(
|
|
||||||
var start : Int,
|
|
||||||
var end : Int,
|
|
||||||
val span : Any
|
|
||||||
)
|
|
||||||
|
|
||||||
// 文字装飾の指定を溜めておいてノードの親子関係に応じて順序を調整して、最後にまとめて適用する
|
// 文字装飾の指定を溜めておいてノードの親子関係に応じて順序を調整して、最後にまとめて適用する
|
||||||
class SpanList {
|
class SpanList {
|
||||||
|
|
||||||
val list = LinkedList<SpanPos>()
|
private class SpanPos(var start : Int, var end : Int, val span : Any)
|
||||||
|
|
||||||
|
private val list = LinkedList<SpanPos>()
|
||||||
|
|
||||||
|
fun setSpan(sb : SpannableStringBuilder) =
|
||||||
|
list.forEach { sb.setSpan(it.span, it.start, it.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) }
|
||||||
|
|
||||||
|
fun addAll(other : SpanList) = list.addAll(other.list)
|
||||||
|
|
||||||
|
fun addWithOffset(src : SpanList, offset : Int) {
|
||||||
|
src.list.forEach { addLast(it.start + offset, it.end + offset, it.span) }
|
||||||
|
}
|
||||||
|
|
||||||
fun addFirst(start : Int, end : Int, span : Any) = when {
|
fun addFirst(start : Int, end : Int, span : Any) = when {
|
||||||
start == end -> {
|
start == end -> {
|
||||||
|
@ -77,12 +83,6 @@ class SpanList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
fun insert(offset : Int, length : Int) {
|
||||||
for(sp in list) {
|
for(sp in list) {
|
||||||
when {
|
when {
|
||||||
|
@ -102,12 +102,13 @@ class SpanList {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matcher.usePattern does re-create nativeImpl
|
// 正規表現パターンごとにMatcherをキャッシュする
|
||||||
// use thread-local cache for each pattern to avoid it
|
// 対象テキストが変わったらキャッシュを捨てて更新する
|
||||||
// this cache keep 1 matcher for each pattern.
|
// Matcher#region(start,text.length) を設定してから返す
|
||||||
// if text is changed, matcher is dropped and re-created.
|
// (同一テキストに対してMatcher.usePatternで正規表現パターンを切り替えるのも検討したが、usePatternの方が多分遅くなる)
|
||||||
internal object MatcherCache {
|
internal object MatcherCache {
|
||||||
|
|
||||||
private class MatcherCacheItem(
|
private class MatcherCacheItem(
|
||||||
|
@ -116,7 +117,9 @@ internal object MatcherCache {
|
||||||
var textHashCode : Int
|
var textHashCode : Int
|
||||||
)
|
)
|
||||||
|
|
||||||
private val matcherCache = object : ThreadLocal<HashMap<Pattern, MatcherCacheItem>>() {
|
// スレッドごとにキャッシュ用のマップを持つ
|
||||||
|
private val matcherCache =
|
||||||
|
object : ThreadLocal<HashMap<Pattern, MatcherCacheItem>>() {
|
||||||
override fun initialValue() : HashMap<Pattern, MatcherCacheItem> = HashMap()
|
override fun initialValue() : HashMap<Pattern, MatcherCacheItem> = HashMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,16 +250,12 @@ object MisskeySyntaxHighlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val symbolMap = SparseBooleanArray().apply {
|
private val symbolMap = SparseBooleanArray().apply {
|
||||||
for(c in "=+-*/%~^&|><!?") {
|
"=+-*/%~^&|><!?".forEach { put(it.toInt(), true) }
|
||||||
this.put(c.toInt(), true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文字列リテラルの開始文字のマップ
|
// 文字列リテラルの開始文字のマップ
|
||||||
private val stringStart = SparseBooleanArray().apply {
|
private val stringStart = SparseBooleanArray().apply {
|
||||||
for(c in "\"'`") {
|
"\"'`".forEach { put(it.toInt(), true) }
|
||||||
this.put(c.toInt(), true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Token(
|
private class Token(
|
||||||
|
@ -350,31 +349,31 @@ object MisskeySyntaxHighlighter {
|
||||||
Pattern.compile("""\A([A-Z_-][A-Z0-9_-]*)([ \t]*\()?""", Pattern.CASE_INSENSITIVE)
|
Pattern.compile("""\A([A-Z_-][A-Z0-9_-]*)([ \t]*\()?""", Pattern.CASE_INSENSITIVE)
|
||||||
private val reContainsAlpha = Pattern.compile("""[A-Za-z_]""")
|
private val reContainsAlpha = Pattern.compile("""[A-Za-z_]""")
|
||||||
|
|
||||||
private val charH80 = 0x80.toChar()
|
private const val charH80 = 0x80.toChar()
|
||||||
|
|
||||||
private val elements = arrayOf<Env.() -> Token?>(
|
private val elements = arrayOf<Env.() -> Token?>(
|
||||||
|
|
||||||
// マルチバイト文字をまとめて読み飛ばす
|
// マルチバイト文字をまとめて読み飛ばす
|
||||||
{
|
{
|
||||||
var s = pos
|
var s = pos
|
||||||
while( s < end && source[s] >= charH80){
|
while(s < end && source[s] >= charH80) {
|
||||||
++s
|
++ s
|
||||||
}
|
}
|
||||||
when{
|
when {
|
||||||
s > pos -> Token(length = s-pos)
|
s > pos -> Token(length = s - pos)
|
||||||
else->null
|
else -> null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 空白と改行をまとめて読み飛ばす
|
// 空白と改行をまとめて読み飛ばす
|
||||||
{
|
{
|
||||||
var s = pos
|
var s = pos
|
||||||
while( s < end && source[s] <= ' '){
|
while(s < end && source[s] <= ' ') {
|
||||||
++s
|
++ s
|
||||||
}
|
}
|
||||||
when{
|
when {
|
||||||
s > pos -> Token(length = s-pos)
|
s > pos -> Token(length = s - pos)
|
||||||
else->null
|
else -> null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -383,7 +382,7 @@ object MisskeySyntaxHighlighter {
|
||||||
val match = remainMatcher(reLineComment)
|
val match = remainMatcher(reLineComment)
|
||||||
when {
|
when {
|
||||||
! match.find() -> null
|
! match.find() -> null
|
||||||
else -> Token(length = match.end()-match.start(), comment = true)
|
else -> Token(length = match.end() - match.start(), comment = true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -392,7 +391,7 @@ object MisskeySyntaxHighlighter {
|
||||||
val match = remainMatcher(reBlockComment)
|
val match = remainMatcher(reBlockComment)
|
||||||
when {
|
when {
|
||||||
! match.find() -> null
|
! match.find() -> null
|
||||||
else -> Token(length = match.end()-match.start(), comment = true)
|
else -> Token(length = match.end() - match.start(), comment = true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -518,15 +517,14 @@ object MisskeySyntaxHighlighter {
|
||||||
when {
|
when {
|
||||||
symbolMap.get(c.toInt(), false) ->
|
symbolMap.get(c.toInt(), false) ->
|
||||||
Token(length = 1, color = 0x42b983)
|
Token(length = 1, color = 0x42b983)
|
||||||
c =='-' ->
|
c == '-' ->
|
||||||
Token(length = 1, color = 0x42b983)
|
Token(length = 1, color = 0x42b983)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
fun parse(source : String) = Env(source,0,source.length).parse()
|
fun parse(source : String) = Env(source, 0, source.length).parse()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object MisskeyMarkdownDecoder {
|
object MisskeyMarkdownDecoder {
|
||||||
|
@ -547,6 +545,7 @@ object MisskeyMarkdownDecoder {
|
||||||
s.replace(reStartEmptyLines, "")
|
s.replace(reStartEmptyLines, "")
|
||||||
.replace(reEndEmptyLines, "")
|
.replace(reEndEmptyLines, "")
|
||||||
|
|
||||||
|
// URLを適当に短くする
|
||||||
private fun shortenUrl(display_url : String) : String {
|
private fun shortenUrl(display_url : String) : String {
|
||||||
return try {
|
return try {
|
||||||
val uri = Uri.parse(display_url)
|
val uri = Uri.parse(display_url)
|
||||||
|
@ -569,12 +568,12 @@ object MisskeyMarkdownDecoder {
|
||||||
}
|
}
|
||||||
sbTmp.toString()
|
sbTmp.toString()
|
||||||
} catch(ex : Throwable) {
|
} catch(ex : Throwable) {
|
||||||
log.trace(ex)
|
MisskeyMarkdownDecoder.log.trace(ex)
|
||||||
display_url
|
display_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// マークダウン要素のデコード時に使う作業変数をまとめたクラス
|
// 装飾つきテキストの出力時に使うデータの集まり
|
||||||
internal class SpanOutputEnv(
|
internal class SpanOutputEnv(
|
||||||
val options : DecodeOptions,
|
val options : DecodeOptions,
|
||||||
val sb : SpannableStringBuilderEx
|
val sb : SpannableStringBuilderEx
|
||||||
|
@ -608,7 +607,7 @@ object MisskeyMarkdownDecoder {
|
||||||
val parent_result = this.spanList
|
val parent_result = this.spanList
|
||||||
parent.childNodes.forEach {
|
parent.childNodes.forEach {
|
||||||
val child_result = fireRender(it)
|
val child_result = fireRender(it)
|
||||||
parent_result.list.addAll(child_result.list)
|
parent_result.addAll(child_result)
|
||||||
}
|
}
|
||||||
this.spanList = parent_result
|
this.spanList = parent_result
|
||||||
return parent_result
|
return parent_result
|
||||||
|
@ -624,7 +623,10 @@ object MisskeyMarkdownDecoder {
|
||||||
|
|
||||||
fun closeBlock() {
|
fun closeBlock() {
|
||||||
if(sb.length > 0 && sb[sb.length - 1] != '\n') {
|
if(sb.length > 0 && sb[sb.length - 1] != '\n') {
|
||||||
|
val start = sb.length
|
||||||
sb.append('\n')
|
sb.append('\n')
|
||||||
|
val end = sb.length
|
||||||
|
sb.setSpan(RelativeSizeSpan(0.1f), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -720,31 +722,21 @@ object MisskeyMarkdownDecoder {
|
||||||
mixColor(Color.GRAY, 0x8000ff)
|
mixColor(Color.GRAY, 0x8000ff)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun <T> hashSetOf(vararg values : T) = HashSet<T>().apply { addAll(values) }
|
// ノード種別とレンダリング関数
|
||||||
|
enum class NodeType(val render : SpanOutputEnv.(Node) -> Unit) {
|
||||||
|
|
||||||
enum class NodeType(
|
TEXT({
|
||||||
val allowInside : Set<NodeType> = emptySet(),
|
appendText(it.args[0], decodeEmoji = true)
|
||||||
val allowInsideAll : Boolean = false,
|
}),
|
||||||
val render : SpanOutputEnv.(Node) -> Unit
|
|
||||||
) {
|
|
||||||
/////////////////////////////////////////////
|
|
||||||
// 入れ子なし
|
|
||||||
|
|
||||||
TEXT(
|
EMOJI({
|
||||||
render = { appendText(it.args[0], decodeEmoji = true) }
|
|
||||||
),
|
|
||||||
|
|
||||||
EMOJI(
|
|
||||||
render = {
|
|
||||||
val code = it.args[0]
|
val code = it.args[0]
|
||||||
if(code.isNotEmpty()) {
|
if(code.isNotEmpty()) {
|
||||||
appendText(":$code:", decodeEmoji = true)
|
appendText(":$code:", decodeEmoji = true)
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
MENTION(
|
MENTION({
|
||||||
render = {
|
|
||||||
val username = it.args[0]
|
val username = it.args[0]
|
||||||
val host = it.args[1]
|
val host = it.args[1]
|
||||||
val linkHelper = linkHelper
|
val linkHelper = linkHelper
|
||||||
|
@ -793,11 +785,9 @@ object MisskeyMarkdownDecoder {
|
||||||
, userUrl
|
, userUrl
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
HASHTAG(
|
HASHTAG({
|
||||||
render = {
|
|
||||||
val linkHelper = linkHelper
|
val linkHelper = linkHelper
|
||||||
val tag = it.args[0]
|
val tag = it.args[0]
|
||||||
if(tag.isNotEmpty() && linkHelper != null) {
|
if(tag.isNotEmpty() && linkHelper != null) {
|
||||||
|
@ -806,47 +796,38 @@ object MisskeyMarkdownDecoder {
|
||||||
"https://${linkHelper.host}/tags/" + tag.encodePercent()
|
"https://${linkHelper.host}/tags/" + tag.encodePercent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
CODE_INLINE(
|
CODE_INLINE({
|
||||||
render = {
|
|
||||||
val text = it.args[0]
|
val text = it.args[0]
|
||||||
val sp = MisskeySyntaxHighlighter.parse(text)
|
val sp = MisskeySyntaxHighlighter.parse(text)
|
||||||
appendText(text)
|
appendText(text)
|
||||||
spanList.addWithOffset(sp.list, start)
|
spanList.addWithOffset(sp, start)
|
||||||
spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080))
|
spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080))
|
||||||
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
URL(
|
URL({
|
||||||
render = {
|
|
||||||
val url = it.args[0]
|
val url = it.args[0]
|
||||||
if(url.isNotEmpty()) {
|
if(url.isNotEmpty()) {
|
||||||
appendLink(url, url, allowShort = true)
|
appendLink(url, url, allowShort = true)
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
CODE_BLOCK(
|
CODE_BLOCK({
|
||||||
|
|
||||||
render = {
|
|
||||||
closePreviousBlock()
|
closePreviousBlock()
|
||||||
|
|
||||||
val text = trimBlock(it.args[0])
|
val text = trimBlock(it.args[0])
|
||||||
val sp = MisskeySyntaxHighlighter.parse(text)
|
val sp = MisskeySyntaxHighlighter.parse(text)
|
||||||
appendText(text)
|
appendText(text)
|
||||||
spanList.addWithOffset(sp.list, start)
|
spanList.addWithOffset(sp, start)
|
||||||
spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080))
|
spanList.addLast(start, sb.length, BackgroundColorSpan(0x40808080))
|
||||||
spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(0.7f))
|
spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(0.7f))
|
||||||
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(Typeface.MONOSPACE))
|
||||||
closeBlock()
|
closeBlock()
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
QUOTE_INLINE(
|
QUOTE_INLINE({
|
||||||
render = {
|
|
||||||
val text = trimBlock(it.args[0])
|
val text = trimBlock(it.args[0])
|
||||||
appendText(text)
|
appendText(text)
|
||||||
spanList.addLast(
|
spanList.addLast(
|
||||||
|
@ -859,11 +840,9 @@ object MisskeyMarkdownDecoder {
|
||||||
sb.length,
|
sb.length,
|
||||||
CalligraphyTypefaceSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC))
|
CalligraphyTypefaceSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC))
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
SEARCH(
|
SEARCH({
|
||||||
render = {
|
|
||||||
closePreviousBlock()
|
closePreviousBlock()
|
||||||
|
|
||||||
val text = it.args[0]
|
val text = it.args[0]
|
||||||
|
@ -879,35 +858,21 @@ object MisskeyMarkdownDecoder {
|
||||||
spanList.addLast(kw_start, sb.length, android.text.style.RelativeSizeSpan(1.2f))
|
spanList.addLast(kw_start, sb.length, android.text.style.RelativeSizeSpan(1.2f))
|
||||||
|
|
||||||
closeBlock()
|
closeBlock()
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
/////////////////////////////////////////////
|
BIG({
|
||||||
// 入れ子あり
|
|
||||||
|
|
||||||
// インライン要素、装飾のみ
|
|
||||||
|
|
||||||
BIG(
|
|
||||||
allowInside = hashSetOf(MENTION, HASHTAG, EMOJI),
|
|
||||||
render = {
|
|
||||||
val start = this.start
|
val start = this.start
|
||||||
fireRenderChildNodes(it)
|
fireRenderChildNodes(it)
|
||||||
spanList.addLast(start, sb.length, MisskeyBigSpan(font_bold))
|
spanList.addLast(start, sb.length, MisskeyBigSpan(font_bold))
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
BOLD(
|
BOLD({
|
||||||
allowInside = hashSetOf(MENTION, HASHTAG, EMOJI),
|
|
||||||
render = {
|
|
||||||
val start = this.start
|
val start = this.start
|
||||||
fireRenderChildNodes(it)
|
fireRenderChildNodes(it)
|
||||||
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(font_bold))
|
spanList.addLast(start, sb.length, CalligraphyTypefaceSpan(font_bold))
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
MOTION(
|
MOTION({
|
||||||
allowInside = hashSetOf(BOLD, MENTION, HASHTAG, EMOJI),
|
|
||||||
render = {
|
|
||||||
val start = this.start
|
val start = this.start
|
||||||
fireRenderChildNodes(it)
|
fireRenderChildNodes(it)
|
||||||
spanList.addFirst(
|
spanList.addFirst(
|
||||||
|
@ -915,19 +880,9 @@ object MisskeyMarkdownDecoder {
|
||||||
sb.length,
|
sb.length,
|
||||||
jp.juggler.subwaytooter.span.MisskeyMotionSpan(jp.juggler.subwaytooter.ActMain.timeline_font)
|
jp.juggler.subwaytooter.span.MisskeyMotionSpan(jp.juggler.subwaytooter.ActMain.timeline_font)
|
||||||
)
|
)
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
// リンクなどのデータを扱う要素
|
LINK({
|
||||||
|
|
||||||
LINK(
|
|
||||||
allowInside = hashSetOf(
|
|
||||||
BIG,
|
|
||||||
BOLD,
|
|
||||||
MOTION,
|
|
||||||
EMOJI
|
|
||||||
),
|
|
||||||
render = {
|
|
||||||
val url = it.args[1]
|
val url = it.args[1]
|
||||||
// val silent = data?.get(2)
|
// val silent = data?.get(2)
|
||||||
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
|
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
|
||||||
|
@ -948,22 +903,9 @@ object MisskeyMarkdownDecoder {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
TITLE(
|
TITLE({
|
||||||
allowInside = hashSetOf(
|
|
||||||
BIG,
|
|
||||||
BOLD,
|
|
||||||
MOTION,
|
|
||||||
URL,
|
|
||||||
LINK,
|
|
||||||
MENTION,
|
|
||||||
HASHTAG,
|
|
||||||
EMOJI,
|
|
||||||
CODE_INLINE
|
|
||||||
),
|
|
||||||
render = {
|
|
||||||
closePreviousBlock()
|
closePreviousBlock()
|
||||||
|
|
||||||
val start = this.start
|
val start = this.start
|
||||||
|
@ -981,12 +923,30 @@ object MisskeyMarkdownDecoder {
|
||||||
spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(1.5f))
|
spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(1.5f))
|
||||||
|
|
||||||
closeBlock()
|
closeBlock()
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
|
|
||||||
QUOTE_BLOCK(
|
CENTER({
|
||||||
allowInsideAll = true,
|
closePreviousBlock()
|
||||||
render = {
|
|
||||||
|
val start = this.start
|
||||||
|
fireRenderChildNodes(it)
|
||||||
|
if(it.quoteNest > 0) {
|
||||||
|
// 引用ネストの内部ではセンタリングさせると引用マーカーまで動いてしまうので
|
||||||
|
// センタリングが機能しないようにする
|
||||||
|
} else {
|
||||||
|
spanList.addLast(
|
||||||
|
start,
|
||||||
|
sb.length,
|
||||||
|
android.text.style.AlignmentSpan.Standard(
|
||||||
|
android.text.Layout.Alignment.ALIGN_CENTER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeBlock()
|
||||||
|
}),
|
||||||
|
|
||||||
|
QUOTE_BLOCK({
|
||||||
closePreviousBlock()
|
closePreviousBlock()
|
||||||
|
|
||||||
val start = this.start
|
val start = this.start
|
||||||
|
@ -1033,37 +993,64 @@ object MisskeyMarkdownDecoder {
|
||||||
)
|
)
|
||||||
|
|
||||||
closeBlock()
|
closeBlock()
|
||||||
|
}),
|
||||||
|
|
||||||
|
ROOT({
|
||||||
|
fireRenderChildNodes(it)
|
||||||
|
}),
|
||||||
|
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
// あるノードが内部に持てるノード種別のマップ
|
||||||
|
val mapAllowInside = HashMap<NodeType, HashSet<NodeType>>().apply {
|
||||||
|
|
||||||
|
fun <T> hashSetOf(vararg values : T) = HashSet<T>().apply { addAll(values) }
|
||||||
|
|
||||||
|
infix fun NodeType.wraps(inner : HashSet<NodeType>) = put(this, inner)
|
||||||
|
|
||||||
|
BIG wraps
|
||||||
|
hashSetOf(EMOJI, HASHTAG, MENTION)
|
||||||
|
BOLD wraps
|
||||||
|
hashSetOf(EMOJI, HASHTAG, MENTION, URL, LINK)
|
||||||
|
MOTION wraps
|
||||||
|
hashSetOf(EMOJI, HASHTAG, MENTION, URL, LINK, BOLD)
|
||||||
|
LINK wraps
|
||||||
|
hashSetOf(EMOJI, MOTION, BIG, BOLD)
|
||||||
|
TITLE wraps
|
||||||
|
hashSetOf(EMOJI, HASHTAG, MENTION, URL, LINK, BIG, BOLD, MOTION, CODE_INLINE)
|
||||||
|
CENTER wraps
|
||||||
|
hashSetOf(EMOJI, HASHTAG, MENTION, URL, LINK, BIG, BOLD, MOTION, CODE_INLINE)
|
||||||
|
QUOTE_BLOCK wraps
|
||||||
|
hashSetOf(
|
||||||
|
EMOJI, HASHTAG, MENTION, URL, LINK, BIG, BOLD, MOTION, CODE_INLINE,
|
||||||
|
CODE_BLOCK, QUOTE_INLINE, SEARCH, TITLE, CENTER, QUOTE_BLOCK
|
||||||
|
)
|
||||||
|
ROOT wraps
|
||||||
|
hashSetOf(
|
||||||
|
EMOJI, HASHTAG, MENTION, URL, LINK, BIG, BOLD, MOTION, CODE_INLINE,
|
||||||
|
CODE_BLOCK, QUOTE_INLINE, SEARCH, TITLE, CENTER, QUOTE_BLOCK
|
||||||
|
)
|
||||||
}
|
}
|
||||||
),
|
|
||||||
|
|
||||||
ROOT(
|
|
||||||
allowInsideAll = true,
|
|
||||||
render = { fireRenderChildNodes(it) }
|
|
||||||
),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
val nodeTypeAllSet = HashSet<NodeType>().apply {
|
|
||||||
for(v in NodeType.values()) {
|
|
||||||
this.add(v)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// マークダウン要素
|
||||||
class Node(
|
class Node(
|
||||||
val type : NodeType, // ノード種別
|
val type : NodeType, // ノード種別
|
||||||
val args : Array<String> = emptyArray(), // 引数
|
val args : Array<String> = emptyArray(), // 引数
|
||||||
parentNode : Node?
|
parentNode : Node?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val childNodes = LinkedList<Node>()
|
val childNodes = LinkedList<Node>()
|
||||||
|
|
||||||
internal val quoteNest : Int = (parentNode?.quoteNest ?: 0) + when(type) {
|
internal val quoteNest : Int = (parentNode?.quoteNest ?: 0) + when(type) {
|
||||||
NodeType.QUOTE_BLOCK, NodeType.QUOTE_INLINE -> 1
|
NodeType.QUOTE_BLOCK, NodeType.QUOTE_INLINE -> 1
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// マークダウン要素の出現位置
|
||||||
class NodeDetected(
|
class NodeDetected(
|
||||||
val node : Node,
|
val node : Node,
|
||||||
val start : Int, // テキスト中の開始位置
|
val start : Int, // テキスト中の開始位置
|
||||||
|
@ -1072,10 +1059,8 @@ object MisskeyMarkdownDecoder {
|
||||||
val startInside : Int, // 内部範囲の開始位置
|
val startInside : Int, // 内部範囲の開始位置
|
||||||
private val lengthInside : Int // 内部範囲の終了位置
|
private val lengthInside : Int // 内部範囲の終了位置
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val endInside : Int
|
val endInside : Int
|
||||||
get() = startInside + lengthInside
|
get() = startInside + lengthInside
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NodeParseEnv(
|
class NodeParseEnv(
|
||||||
|
@ -1086,11 +1071,8 @@ object MisskeyMarkdownDecoder {
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val childNodes = parentNode.childNodes
|
private val childNodes = parentNode.childNodes
|
||||||
private val allowInside = if(parentNode.type.allowInsideAll) {
|
private val allowInside : HashSet<NodeType> =
|
||||||
nodeTypeAllSet
|
NodeType.mapAllowInside[parentNode.type] ?: hashSetOf()
|
||||||
} else {
|
|
||||||
parentNode.type.allowInside
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直前のノードの終了位置
|
// 直前のノードの終了位置
|
||||||
internal var lastEnd = start
|
internal var lastEnd = start
|
||||||
|
@ -1220,7 +1202,10 @@ object MisskeyMarkdownDecoder {
|
||||||
// (マークダウン要素の特徴的な文字)と(パーサ関数の配列)のマップ
|
// (マークダウン要素の特徴的な文字)と(パーサ関数の配列)のマップ
|
||||||
private val nodeParserMap = SparseArray<Array<out NodeParseEnv.() -> NodeDetected?>>().apply {
|
private val nodeParserMap = SparseArray<Array<out NodeParseEnv.() -> NodeDetected?>>().apply {
|
||||||
|
|
||||||
fun addParser(firstChars : String, vararg nodeParsers : NodeParseEnv.() -> NodeDetected?) {
|
fun addParser(
|
||||||
|
firstChars : String,
|
||||||
|
vararg nodeParsers : NodeParseEnv.() -> NodeDetected?
|
||||||
|
) {
|
||||||
for(s in firstChars) {
|
for(s in firstChars) {
|
||||||
put(s.toInt(), nodeParsers)
|
put(s.toInt(), nodeParsers)
|
||||||
}
|
}
|
||||||
|
@ -1230,16 +1215,18 @@ object MisskeyMarkdownDecoder {
|
||||||
addParser(
|
addParser(
|
||||||
"\""
|
"\""
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
Pattern.compile("""^"([^\x0d\x0a]+?)\n"[\x0d\x0a]*""")
|
Pattern.compile("""\A"([^\x0d\x0a]+?)\n"[\x0d\x0a]*""")
|
||||||
, NodeType.QUOTE_INLINE
|
, NodeType.QUOTE_INLINE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Quote (行頭)>...(改行)
|
// Quote (行頭)>...(改行)
|
||||||
val reQuoteBlock = Pattern.compile(
|
val reQuoteBlock = Pattern.compile(
|
||||||
"^>(?:[ ]?)([^\\x0d\\x0a]*)(\\x0a|\\x0d\\x0a?)?",
|
// この正規表現の場合は \A ではなく ^ で各行の始端にマッチさせる
|
||||||
|
"""^>(?:[ ]?)([^\x0d\x0a]*)(\x0a|\x0d\x0a?)?""",
|
||||||
Pattern.MULTILINE
|
Pattern.MULTILINE
|
||||||
)
|
)
|
||||||
|
|
||||||
addParser(">", {
|
addParser(">", {
|
||||||
if(pos > 0) {
|
if(pos > 0) {
|
||||||
val c = text[pos - 1]
|
val c = text[pos - 1]
|
||||||
|
@ -1281,7 +1268,7 @@ object MisskeyMarkdownDecoder {
|
||||||
addParser(
|
addParser(
|
||||||
":"
|
":"
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
Pattern.compile("""^:([a-zA-Z0-9+-_]+):""")
|
Pattern.compile("""\A:([a-zA-Z0-9+-_]+):""")
|
||||||
, NodeType.EMOJI
|
, NodeType.EMOJI
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1289,7 +1276,7 @@ object MisskeyMarkdownDecoder {
|
||||||
addParser(
|
addParser(
|
||||||
"("
|
"("
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
Pattern.compile("""^\Q(((\E(.+?)\Q)))\E""")
|
Pattern.compile("""\A\Q(((\E(.+?)\Q)))\E""", Pattern.DOTALL)
|
||||||
, NodeType.MOTION
|
, NodeType.MOTION
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1297,9 +1284,13 @@ object MisskeyMarkdownDecoder {
|
||||||
addParser(
|
addParser(
|
||||||
"<"
|
"<"
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
Pattern.compile("""^<motion>(.+?)</motion>""")
|
Pattern.compile("""\A<motion>(.+?)</motion>""", Pattern.DOTALL)
|
||||||
, NodeType.MOTION
|
, NodeType.MOTION
|
||||||
)
|
)
|
||||||
|
, simpleParser(
|
||||||
|
Pattern.compile("""\A<center>(.+?)</center>""", Pattern.DOTALL)
|
||||||
|
, NodeType.CENTER
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ***big*** **bold**
|
// ***big*** **bold**
|
||||||
|
@ -1321,7 +1312,7 @@ object MisskeyMarkdownDecoder {
|
||||||
addParser(
|
addParser(
|
||||||
"h"
|
"h"
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
Pattern.compile("""^(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
|
Pattern.compile("""\A(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
|
||||||
, NodeType.URL
|
, NodeType.URL
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1329,7 +1320,7 @@ object MisskeyMarkdownDecoder {
|
||||||
// 検索
|
// 検索
|
||||||
|
|
||||||
val reSearchButton = Pattern.compile(
|
val reSearchButton = Pattern.compile(
|
||||||
"""^(検索|\[検索]|Search|\[Search])(\n|${'$'})"""
|
"""\A(検索|\[検索]|Search|\[Search])(\n|${'$'})"""
|
||||||
, Pattern.CASE_INSENSITIVE
|
, Pattern.CASE_INSENSITIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1358,7 +1349,7 @@ object MisskeyMarkdownDecoder {
|
||||||
else -> makeDetected(
|
else -> makeDetected(
|
||||||
NodeType.SEARCH,
|
NodeType.SEARCH,
|
||||||
arrayOf(keyword),
|
arrayOf(keyword),
|
||||||
pos - (keyword.length + 1),matcher.end(),
|
pos - (keyword.length + 1), matcher.end(),
|
||||||
this.text, pos - (keyword.length + 1), keyword.length
|
this.text, pos - (keyword.length + 1), keyword.length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1369,13 +1360,13 @@ object MisskeyMarkdownDecoder {
|
||||||
// [title] 【title】
|
// [title] 【title】
|
||||||
// 直後に改行が必要だったが文末でも良いことになった https://github.com/syuilo/misskey/commit/79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae
|
// 直後に改行が必要だったが文末でも良いことになった https://github.com/syuilo/misskey/commit/79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae
|
||||||
val titleParser = simpleParser(
|
val titleParser = simpleParser(
|
||||||
Pattern.compile("""^[【\[](.+?)[】\]](\n|\z)""")
|
Pattern.compile("""\A[【\[](.+?)[】\]](\n|\z)""")
|
||||||
, NodeType.TITLE
|
, NodeType.TITLE
|
||||||
)
|
)
|
||||||
|
|
||||||
// Link
|
// Link
|
||||||
val reLink = Pattern.compile(
|
val reLink = Pattern.compile(
|
||||||
"""^\??\[([^\[\]]+?)]\((https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+?)\)"""
|
"""\A\??\[([^\n\[\]]+?)]\((https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+?)\)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
val linkParser : NodeParseEnv.() -> NodeDetected? = {
|
val linkParser : NodeParseEnv.() -> NodeDetected? = {
|
||||||
|
@ -1409,7 +1400,7 @@ object MisskeyMarkdownDecoder {
|
||||||
|
|
||||||
// メンション @username @username@host
|
// メンション @username @username@host
|
||||||
val reMention = Pattern.compile(
|
val reMention = Pattern.compile(
|
||||||
"""^@([a-z0-9_]+)(?:@([a-z0-9.\-]+[a-z0-9]))?"""
|
"""\A@([a-z0-9_]+)(?:@([a-z0-9.\-]+[a-z0-9]))?"""
|
||||||
, Pattern.CASE_INSENSITIVE
|
, Pattern.CASE_INSENSITIVE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1430,7 +1421,7 @@ object MisskeyMarkdownDecoder {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hashtag
|
// Hashtag
|
||||||
val reHashtag = Pattern.compile("""^#([^\s]+)""")
|
val reHashtag = Pattern.compile("""\A#([^\s]+)""")
|
||||||
addParser("#"
|
addParser("#"
|
||||||
, {
|
, {
|
||||||
val matcher = remainMatcher(reHashtag)
|
val matcher = remainMatcher(reHashtag)
|
||||||
|
@ -1457,7 +1448,7 @@ object MisskeyMarkdownDecoder {
|
||||||
addParser(
|
addParser(
|
||||||
"`"
|
"`"
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
Pattern.compile("""^```(?:.*)\n([\s\S]+?)\n```(?:\n|$)""")
|
Pattern.compile("""\A```(?:.*)\n([\s\S]+?)\n```(?:\n|$)""")
|
||||||
, NodeType.CODE_BLOCK
|
, NodeType.CODE_BLOCK
|
||||||
/*
|
/*
|
||||||
(A)
|
(A)
|
||||||
|
@ -1475,7 +1466,7 @@ object MisskeyMarkdownDecoder {
|
||||||
)
|
)
|
||||||
, simpleParser(
|
, simpleParser(
|
||||||
// インラインコードは内部にとある文字を含むと認識されない。理由は顔文字と衝突するからだとか
|
// インラインコードは内部にとある文字を含むと認識されない。理由は顔文字と衝突するからだとか
|
||||||
Pattern.compile("""^`([^`´\x0d\x0a]+)`""")
|
Pattern.compile("""\A`([^`´\x0d\x0a]+)`""")
|
||||||
, NodeType.CODE_INLINE
|
, NodeType.CODE_INLINE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1492,9 +1483,8 @@ object MisskeyMarkdownDecoder {
|
||||||
if(src != null) {
|
if(src != null) {
|
||||||
val root = Node(NodeType.ROOT, emptyArray(), null)
|
val root = Node(NodeType.ROOT, emptyArray(), null)
|
||||||
NodeParseEnv(root, src, 0, src.length).parseInside()
|
NodeParseEnv(root, src, 0, src.length).parseInside()
|
||||||
for(sp in env.fireRender(root).list) {
|
env.fireRender(root).setSpan(env.sb)
|
||||||
env.sb.setSpan(sp.span, sp.start, sp.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 末尾の空白を取り除く
|
// 末尾の空白を取り除く
|
||||||
|
|
Loading…
Reference in New Issue