(Misskey)MFMのcenterマークダウンに対応

This commit is contained in:
tateisu 2018-11-26 22:03:21 +09:00
parent 32526a0bb8
commit 4fef191a35
1 changed files with 380 additions and 390 deletions

View File

@ -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)
}
} }
// 末尾の空白を取り除く // 末尾の空白を取り除く