From c24aeeadc46537e382d52c76a322dd90c6b7a228 Mon Sep 17 00:00:00 2001 From: tateisu Date: Fri, 10 Mar 2023 02:13:40 +0900 Subject: [PATCH] =?UTF-8?q?=E6=8A=95=E7=A8=BF=E3=81=AEHTML=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=81=AE=E8=A1=A8=E7=A4=BA=E3=82=92=E5=B0=91?= =?UTF-8?q?=E3=81=97=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subwaytooter/api/entity/TootInstance.kt | 6 +- .../jp/juggler/subwaytooter/mfm/NodeType.kt | 50 ++-- .../subwaytooter/span/BlockCodeSpan.kt | 81 ++++++ .../subwaytooter/span/BlockQuoteSpan.kt | 62 +++++ .../jp/juggler/subwaytooter/span/DdSpan.kt | 35 +++ .../jp/juggler/subwaytooter/span/HrSpan.kt | 72 +++++ .../subwaytooter/span/InlineCodeSpan.kt | 15 ++ .../subwaytooter/span/OrderedListItemSpan.kt | 71 +++++ .../jp/juggler/subwaytooter/span/SpanUtils.kt | 7 + .../span/UnorderedListItemSpan.kt | 86 ++++++ .../juggler/subwaytooter/util/HTMLDecoder.kt | 248 +++++++++--------- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/styles.xml | 11 +- 13 files changed, 591 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/BlockCodeSpan.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/BlockQuoteSpan.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/DdSpan.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/HrSpan.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/InlineCodeSpan.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/OrderedListItemSpan.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/SpanUtils.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/span/UnorderedListItemSpan.kt diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index 7bf0b45f..1bf2f56a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -401,6 +401,8 @@ class TootInstance(parser: TootParser, src: JsonObject) { */ private val reOldMisskeyCompatible = """\A[\d.]+:compatible:misskey:""".toRegex() + private val reBothCompatible = """\b(?:misskey|calckey)\b""".toRegex(RegexOption.IGNORE_CASE) + // 疑似アカウントの追加時に、インスタンスの検証を行う private suspend fun TootApiClient.getInstanceInformation( forceAccessToken: String? = null, @@ -419,7 +421,9 @@ class TootInstance(parser: TootParser, src: JsonObject) { // 他、Mastodonではない場合は kids.0px.io が存在する // https://kids.0px.io/notes/9b628dpesb // Misskey有効トグルで結果を切り替えたいらしい - version.contains("misskey", ignoreCase = true) && + // + // Calckey.jp は調査中 + reBothCompatible.containsMatchIn(version) && PrefB.bpEnableDeprecatedSomething.value -> Unit // 両方のAPIに応答するサーバは他にないと思う。 diff --git a/app/src/main/java/jp/juggler/subwaytooter/mfm/NodeType.kt b/app/src/main/java/jp/juggler/subwaytooter/mfm/NodeType.kt index af4eefce..fc46875b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/mfm/NodeType.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/mfm/NodeType.kt @@ -1,5 +1,8 @@ package jp.juggler.subwaytooter.mfm +import android.graphics.Typeface +import jp.juggler.subwaytooter.span.BlockCodeSpan +import jp.juggler.subwaytooter.span.BlockQuoteSpan import jp.juggler.subwaytooter.table.daoAcctColor import jp.juggler.util.data.encodePercent import jp.juggler.util.data.notEmpty @@ -51,7 +54,7 @@ enum class NodeType(val render: SpanOutputEnv.(Node) -> Unit) { spanList.addLast(start, sb.length, android.text.style.BackgroundColorSpan(0x40808080)) spanList.addLast( start, sb.length, - fontSpan(android.graphics.Typeface.MONOSPACE) + fontSpan(Typeface.MONOSPACE) ) } }), @@ -72,12 +75,8 @@ enum class NodeType(val render: SpanOutputEnv.(Node) -> Unit) { val sp = MisskeySyntaxHighlighter.parse(text) appendText(text) spanList.addWithOffset(sp, start) - spanList.addLast(start, sb.length, android.text.style.BackgroundColorSpan(0x40808080)) - spanList.addLast(start, sb.length, android.text.style.RelativeSizeSpan(0.7f)) - spanList.addLast( - start, sb.length, - fontSpan(android.graphics.Typeface.MONOSPACE) - ) + + spanList.addLast(start, sb.length, BlockCodeSpan()) closeBlock() } }), @@ -96,7 +95,7 @@ enum class NodeType(val render: SpanOutputEnv.(Node) -> Unit) { spanList.addLast( start, sb.length, - fontSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC)) + fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) ) } }), @@ -189,7 +188,7 @@ enum class NodeType(val render: SpanOutputEnv.(Node) -> Unit) { fireRenderChildNodes(it) spanList.addLast( start, sb.length, - fontSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC)) + fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)) ) } }), @@ -310,34 +309,21 @@ enum class NodeType(val render: SpanOutputEnv.(Node) -> Unit) { val bgColor = MisskeyMarkdownDecoder.quoteNestColors[it.quoteNest % MisskeyMarkdownDecoder.quoteNestColors.size] - // 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(bgColor) - ) - } - } spanList.addLast( start, sb.length, - fontSpan(android.graphics.Typeface.defaultFromStyle(android.graphics.Typeface.ITALIC)) + BlockQuoteSpan(context = context, blockQuoteColor = bgColor) + ) + spanList.addLast( + start, + sb.length, + fontSpan( + Typeface.defaultFromStyle( + Typeface.ITALIC + ) + ) ) - closeBlock() } }), diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/BlockCodeSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/BlockCodeSpan.kt new file mode 100644 index 00000000..f518fd48 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/BlockCodeSpan.kt @@ -0,0 +1,81 @@ +package jp.juggler.subwaytooter.span + + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface +import android.text.Layout +import android.text.TextPaint +import android.text.style.LeadingMarginSpan +import android.text.style.MetricAffectingSpan +import jp.juggler.subwaytooter.R +import jp.juggler.subwaytooter.pref.lazyContext +import jp.juggler.util.ui.attrColor +import kotlin.math.max +import kotlin.math.min + +/** + * コードブロック用の装飾スパン + */ +class BlockCodeSpan ( + context: Context = lazyContext, + private var typeface: Typeface = Typeface.MONOSPACE, + private var relativeTextSize: Float = 0.7f, + private var margin: Int = 0, + private var textColor: Int = context.attrColor(R.attr.colorTextContent), + private var backgroundColor: Int = 0x40808080, +) : MetricAffectingSpan(), LeadingMarginSpan { + + private val rect = Rect() + private val paint = Paint() + + private fun apply(paint: TextPaint) { + paint.color = textColor + paint.typeface = typeface + paint.textSize = paint.textSize * relativeTextSize + } + + override fun updateDrawState(tp: TextPaint) { + apply(tp) + } + + override fun updateMeasureState(textPaint: TextPaint) { + apply(textPaint) + } + + override fun getLeadingMargin(first: Boolean): Int { + return margin + } + + override fun drawLeadingMargin( + c: Canvas, + p: Paint, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence?, + start: Int, + end: Int, + first: Boolean, + layout: Layout + ) { + paint.style = Paint.Style.FILL + paint.color = backgroundColor + + val line = layout.getLineForOffset(start) + val left = layout.getParagraphLeft(line) + val right = layout.getParagraphRight(line) + rect.set( + min(left,right), + top, + max(left,right), + bottom, + ) + c.drawRect(rect, paint) + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/BlockQuoteSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/BlockQuoteSpan.kt new file mode 100644 index 00000000..46906c2f --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/BlockQuoteSpan.kt @@ -0,0 +1,62 @@ +package jp.juggler.subwaytooter.span + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.text.Layout +import android.text.style.LeadingMarginSpan +import jp.juggler.subwaytooter.R +import jp.juggler.util.ui.attrColor +import kotlin.math.max +import kotlin.math.min + +/** + * ブロック引用の装飾スパン + */ +class BlockQuoteSpan( + context: Context, + private val margin: Int = (context.resources.displayMetrics.density * 24f + 0.5f).toInt(), + private val quoteWidth: Int = (context.resources.displayMetrics.density * 4f + 0.5f).toInt(), + private val blockQuoteColor: Int = context.attrColor(R.attr.colorTextHint), +) : LeadingMarginSpan { + private val rect = Rect() + private val paint = Paint() + override fun getLeadingMargin(first: Boolean): Int = margin + + override fun drawLeadingMargin( + c: Canvas, + p: Paint, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence, + start: Int, + end: Int, + first: Boolean, + layout: Layout, + ) { + paint.set(p) + paint.style = Paint.Style.FILL + paint.color = blockQuoteColor + + val line = layout.getLineForOffset(start) + val edge = if (dir > 0) { + layout.getParagraphLeft(line) + } else { + layout.getParagraphRight(line) + } + val width = quoteWidth + val l = edge - dir * width + val r = edge - dir * width * 2 + rect.set( + min(l, r), + top, + max(l, r), + bottom + ) + c.drawRect(rect, paint) + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/DdSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/DdSpan.kt new file mode 100644 index 00000000..01c33093 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/DdSpan.kt @@ -0,0 +1,35 @@ +package jp.juggler.subwaytooter.span + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.text.Layout +import android.text.style.LeadingMarginSpan + +/** + * dd 要素の字下げスパン + */ +class DdSpan( + context: Context, + marginDp: Float = 24f, +) : LeadingMarginSpan { + private val marginPx = (context.resources.displayMetrics.density * marginDp + 0.5f).toInt() + + override fun getLeadingMargin(first: Boolean): Int = marginPx + + override fun drawLeadingMargin( + c: Canvas, + p: Paint, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence, + start: Int, + end: Int, + first: Boolean, + layout: Layout, + ) { + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/HrSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/HrSpan.kt new file mode 100644 index 00000000..21b74177 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/HrSpan.kt @@ -0,0 +1,72 @@ +package jp.juggler.subwaytooter.span + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.text.Layout +import android.text.TextPaint +import android.text.style.LeadingMarginSpan +import android.text.style.MetricAffectingSpan +import jp.juggler.subwaytooter.R +import jp.juggler.subwaytooter.pref.lazyContext +import jp.juggler.util.ui.attrColor +import kotlin.math.max +import kotlin.math.min + +/** + * コードブロック用の装飾スパン + */ +class HrSpan( + context: Context = lazyContext, + private var lineColor: Int = context.attrColor(R.attr.colorTextContent), + private var lineHeight: Int = (context.resources.displayMetrics.density * 1f + 0.5f).toInt(), +) : MetricAffectingSpan(), LeadingMarginSpan { + + private val rect = Rect() + private val paint = Paint() + + private fun apply(paint: TextPaint) { + paint.color = 0 + } + + override fun updateDrawState(tp: TextPaint) { + apply(tp) + } + + override fun updateMeasureState(textPaint: TextPaint) { + apply(textPaint) + } + + override fun getLeadingMargin(first: Boolean): Int { + return 0 + } + + override fun drawLeadingMargin( + c: Canvas, + p: Paint, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence?, + start: Int, + end: Int, + first: Boolean, + layout: Layout, + ) { + paint.style = Paint.Style.FILL + paint.color = lineColor + + val lineY = (top + bottom).shr(1) + val lineT = lineY - lineHeight.shr(1) + val lineB = lineT + lineHeight + + val line = layout.getLineForOffset(start) + val left = layout.getParagraphLeft(line) + val right = layout.getParagraphRight(line) + rect.set(min(left, right), lineT, max(left, right), lineB) + c.drawRect(rect, paint) + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/InlineCodeSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/InlineCodeSpan.kt new file mode 100644 index 00000000..5bb1a1ea --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/InlineCodeSpan.kt @@ -0,0 +1,15 @@ +package jp.juggler.subwaytooter.span + +import android.graphics.Typeface +import android.text.TextPaint +import io.github.inflationx.calligraphy3.CalligraphyTypefaceSpan + +class InlineCodeSpan( + tf: Typeface =Typeface.MONOSPACE, + private val bgColor :Int = 0x40808080, +) : CalligraphyTypefaceSpan(tf) { + override fun updateDrawState(drawState: TextPaint) { + super.updateDrawState(drawState) + drawState.bgColor = bgColor + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/OrderedListItemSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/OrderedListItemSpan.kt new file mode 100644 index 00000000..3b1af9cb --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/OrderedListItemSpan.kt @@ -0,0 +1,71 @@ +package jp.juggler.subwaytooter.span + +import android.graphics.Canvas +import android.graphics.Paint +import android.text.Layout +import android.text.TextPaint +import android.text.style.LeadingMarginSpan + +class OrderedListItemSpan( + private val order: String, + orders: List, +) : LeadingMarginSpan { + companion object { + fun List.longest(paint: TextPaint): String? { + var longestText: String? = null + var longestTextWidth: Int? = null + forEach { text -> + val textWidth = (paint.measureText(text) + .5f).toInt() + if (longestTextWidth?.takeIf { it > textWidth } != null) return@forEach + longestText = text + longestTextWidth = textWidth + } + return longestText + } + } + + private val paint = TextPaint() + private val longestOrder = orders.longest(paint) ?: "" + private var longestOrderWidth = + (paint.measureText(longestOrder) + .5f).toInt() + + (paint.measureText(". ") * 2f + .5f).toInt() + + override fun getLeadingMargin(first: Boolean): Int = longestOrderWidth + + override fun drawLeadingMargin( + c: Canvas, + p: Paint, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence, + start: Int, + end: Int, + first: Boolean, + layout: Layout, + ) { + // if there was a line break, we don't need to draw anything + if (!first || !selfStart(start, text, this)) { + return + } + + paint.set(p) + + val longestOrderWidth = (paint.measureText(longestOrder) + .5f).toInt() + + (paint.measureText(".") * 2f + .5f).toInt() + this.longestOrderWidth = longestOrderWidth + + val line = layout.getLineForOffset(start) + if (dir > 0) { + paint.textAlign = Paint.Align.LEFT + val left = layout.getParagraphLeft(line) - longestOrderWidth + c.drawText("${order}.", left.toFloat(), baseline.toFloat(), paint) + } else { + paint.textAlign = Paint.Align.RIGHT + val right = layout.getParagraphRight(line) + longestOrderWidth + c.drawText(".${order}", right.toFloat(), baseline.toFloat(), paint) + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/SpanUtils.kt b/app/src/main/java/jp/juggler/subwaytooter/span/SpanUtils.kt new file mode 100644 index 00000000..81a16076 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/SpanUtils.kt @@ -0,0 +1,7 @@ +package jp.juggler.subwaytooter.span + +import android.text.Spanned + +fun selfStart(start: Int, text: CharSequence?, span: Any?): Boolean { + return text is Spanned && text.getSpanStart(span) == start +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/UnorderedListItemSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/UnorderedListItemSpan.kt new file mode 100644 index 00000000..0b488791 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/span/UnorderedListItemSpan.kt @@ -0,0 +1,86 @@ +package jp.juggler.subwaytooter.span + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.text.Layout +import android.text.TextPaint +import android.text.style.LeadingMarginSpan +import androidx.annotation.IntRange + +class UnorderedListItemSpan( + @IntRange(from = 0) private val level: Int, + private var bulletWidth: Int = 0, +) : LeadingMarginSpan { + + private val circle = RectF() + private val rectangle = Rect() + private val paint = TextPaint() + + private var marginWidth = (paint.descent() - paint.ascent() + .5f).toInt() + + override fun getLeadingMargin(first: Boolean) = marginWidth + + override fun drawLeadingMargin( + c: Canvas, + p: Paint, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence, + start: Int, + end: Int, + first: Boolean, + layout: Layout, + ) { + // if there was a line break, we don't need to draw anything + if (!first || !selfStart(start, text, this)) return + + paint.set(p) + val save = c.save() + try { + // テキストの高さ + val textLineHeight = (paint.descent() - paint.ascent() + .5f).toInt() + // ドットを含む横マージンの幅 + @Suppress("UnnecessaryVariable") + val marginWidth = textLineHeight + this.marginWidth = marginWidth + val marginHalf = marginWidth/2 + + // ドットの直径 + val bulletWidth = textLineHeight / 2 + + // 行頭からドットの開始端までの距離 + val marginLeft = (marginWidth - bulletWidth) / 2 + + val line = layout.getLineForOffset(start) + val edge = if (dir > 0) { + layout.getParagraphLeft(line) + } else { + layout.getParagraphRight(line) + } + val l = edge - dir * marginHalf - bulletWidth/2 + val t = + baseline + ((paint.descent() + paint.ascent()) / 2f + .5f).toInt() - bulletWidth / 2 + val r = l + bulletWidth + val b = t + bulletWidth + if (level == 0 || level == 1) { + circle.set(l.toFloat(), t.toFloat(), r.toFloat(), b.toFloat()) + paint.style = when (level) { + 0 -> Paint.Style.FILL + else -> Paint.Style.STROKE + } + c.drawOval(circle, paint) + } else { + rectangle.set(l, t, r, b) + paint.style = Paint.Style.FILL + c.drawRect(rectangle, paint) + } + } finally { + c.restoreToCount(save) + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt index e5789d7a..0ac51a4f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt @@ -17,6 +17,7 @@ import jp.juggler.subwaytooter.api.entity.TootMention import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.mfm.MisskeyMarkdownDecoder import jp.juggler.subwaytooter.pref.PrefB +import jp.juggler.subwaytooter.pref.lazyContext import jp.juggler.subwaytooter.span.* import jp.juggler.subwaytooter.table.HighlightWord import jp.juggler.subwaytooter.table.daoAcctColor @@ -321,13 +322,15 @@ object HTMLDecoder { val nestLevelDefinition: Int, val nestLevelQuote: Int, var order: Int = 0, + val listOrders: List? = null, ) { - fun subOrdered() = ListContext( + fun subOrdered(listOrders:List) = ListContext( type = ListType.Ordered, nestLevelOrdered + 1, nestLevelUnordered, nestLevelDefinition, - nestLevelQuote + nestLevelQuote, + listOrders= listOrders, ) fun subUnordered() = ListContext( @@ -354,12 +357,7 @@ object HTMLDecoder { nestLevelQuote + 1 ) - fun increment() = when (type) { - ListType.Ordered -> "${++order}. " - ListType.Unordered -> "${listMarkers[nestLevelUnordered % listMarkers.size]} " - ListType.Definition -> "" - else -> "" - } + fun increment() = ++order fun inList() = nestLevelOrdered + nestLevelUnordered + nestLevelDefinition > 0 @@ -369,43 +367,6 @@ object HTMLDecoder { } } - // SpannableStringBuilderを行ごとに分解する - // 行末の改行文字は各行の末尾に残る - // 最終行の長さが0(改行文字もなし)だった場合は出力されない - fun SpannableStringBuilder.splitLines() = - ArrayList().also { dst -> - // 入力の末尾のtrim - var end = this.length - while (end > 0 && CharacterGroup.isWhitespace(this[end - 1].code)) --end - - // 入力の最初の非空白文字の位置を調べておく - var firstNonSpace = 0 - while (firstNonSpace < end && CharacterGroup.isWhitespace(this[firstNonSpace].code)) ++firstNonSpace - - var i = 0 - while (i < end) { - val lineStart = i - while (i < end && this[i] != '\n') ++i - val lineEnd = if (i >= end) end else i + 1 - ++i - - // 行頭の空白を削る -// while (lineStart < lineEnd && -// this[lineStart] != '\n' && -// CharacterGroup.isWhitespace(this[lineStart].toInt()) -// ) ++lineStart - - // 最初の非空白文字以降の行を出力する - if (lineEnd > firstNonSpace) { - dst.add(this.subSequence(lineStart, lineEnd) as SpannableStringBuilder) - } - } - if (dst.isEmpty()) { - // ブロック要素は最低1行は存在するので、1行だけの要素を作る - dst.add(SpannableStringBuilder()) - } - } - private val reLastLine = """(?:\A|\n)([^\n]*)\z""".toRegex() private class Node { @@ -413,7 +374,7 @@ object HTMLDecoder { val child_nodes = ArrayList() val tag: String - val text: String + var text: String private val href: String? get() { @@ -569,11 +530,13 @@ object HTMLDecoder { class EncodeSpanEnv( val options: DecodeOptions, val listContext: ListContext, - val tag: String, + val node: Node, val sb: SpannableStringBuilder, val sbTmp: SpannableStringBuilder, val spanStart: Int, - ) + ){ + val tag = node.tag + } val originalFlusher: EncodeSpanEnv.() -> Unit = { when (tag) { @@ -692,41 +655,31 @@ object HTMLDecoder { Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } - "pre" -> { - sb.setSpan( - BackgroundColorSpan(0x40808080), - spanStart, - sb.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - sb.setSpan( - RelativeSizeSpan(0.7f), - spanStart, - sb.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - sb.setSpan( - fontSpan(Typeface.MONOSPACE), - spanStart, - sb.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } "code" -> { + // インラインコード用の装飾 +// sb.setSpan( +// BackgroundColorSpan(0x40808080), +// spanStart, +// sb.length, +// Spannable.SPAN_EXCLUSIVE_EXCLUSIVE +// ) sb.setSpan( - BackgroundColorSpan(0x40808080), - spanStart, - sb.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - sb.setSpan( - fontSpan(Typeface.MONOSPACE), + InlineCodeSpan(), + spanStart, + sb.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + "hr" -> { + val start =sb.length + sb.append("-") + sb.setSpan( + HrSpan(lazyContext), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } - "hr" -> sb.append("----------") } } @@ -736,6 +689,17 @@ object HTMLDecoder { for (tag in tags) this[tag] = block } + fun SpannableStringBuilder.deleteLastSpaces() { + // 最低でも1文字は残す + var last = length - 1 + while (last > 0) { + if (!CharacterGroup.isWhitespace(get(last).code)) break + --last + } + // 末尾の空白を除去 + if (last != length - 1) delete(last + 1, length) + } + add("a") { val linkInfo = formatLinkCaption(options, sbTmp, href ?: "") val caption = linkInfo.caption @@ -780,67 +744,115 @@ object HTMLDecoder { } add("blockquote") { - val bg_color = listContext.quoteColor() - - // TextView の文字装飾では「ブロック要素の入れ子」を表現できない - // 内容の各行の始端に何か追加するというのがまずキツい - // しかし各行の頭に引用マークをつけないと引用のネストで意味が通じなくなってしまう - - val startItalic = sb.length - sbTmp.splitLines().forEach { line -> - val lineStart = sb.length - sb.append("> ") - sb.setSpan( - BackgroundColorSpan(bg_color), - lineStart, - lineStart + 1, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - sb.append(line) - } + val start = sb.length + sbTmp.deleteLastSpaces() + sb.append(sbTmp) + sb.setSpan( + BlockQuoteSpan( + context = lazyContext, + blockQuoteColor = listContext.quoteColor() + ), + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) sb.setSpan( fontSpan(Typeface.defaultFromStyle(Typeface.ITALIC)), - startItalic, + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE, + ) + } + add("pre") { + val start = sb.length + sbTmp.deleteLastSpaces() + // インラインコード用の装飾を除去する + sbTmp.getSpans(0, sbTmp.length, Any::class.java).forEach { span -> + if (span is BackgroundColorSpan && span.backgroundColor == 0x40808080) { + sbTmp.removeSpan(span) + } else if (span is InlineCodeSpan) { + sbTmp.removeSpan(span) + } + } + sb.append(sbTmp) + sb.setSpan( + BlockCodeSpan(), + start, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } - add("li") { - val lineHeader1 = listContext.increment() - val lineHeader2 = " ".repeat(lineHeader1.length) - sbTmp.splitLines().forEachIndexed { i, line -> - sb.append(if (i == 0) lineHeader1 else lineHeader2) - sb.append(line) + sbTmp.deleteLastSpaces() + val start = sb.length + sb.append(sbTmp) + when (listContext.type) { + ListType.Unordered -> { + sb.setSpan( + UnorderedListItemSpan( + level = listContext.nestLevelOrdered, + ), + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + ListType.Ordered -> { + sb.setSpan( + OrderedListItemSpan( + order = node.text, + orders = listContext.listOrders?: listOf(node.text), + ), + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + else ->Unit } } add("dt") { - val prefix = listContext.increment() - val startBold = sb.length - sbTmp.splitLines().forEach { line -> - sb.append(prefix) - sb.append(line) - } + sbTmp.deleteLastSpaces() + val start = sb.length + sb.append(sbTmp) + sb.setSpan( + DdSpan(lazyContext, marginDp = 3f), + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) sb.setSpan( fontSpan(Typeface.defaultFromStyle(Typeface.BOLD)), - startBold, + start, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) } add("dd") { - val prefix = listContext.increment() + "  " - sbTmp.splitLines().forEach { line -> - sb.append(prefix) - sb.append(line) - } + sbTmp.deleteLastSpaces() + val start = sb.length + sb.append(sbTmp) + sb.setSpan( + DdSpan(lazyContext, marginDp = 24f), + start, + sb.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } } - fun childListContext(tag: String, outerContext: ListContext) = when (tag) { - "ol" -> outerContext.subOrdered() + fun childListContext(node:Node, outerContext: ListContext) = when (node.tag) { + "ol" -> { + var n = 1 + val reversed = false + val listItems = node.child_nodes.filter { it.tag == "li" } + (if(reversed ) listItems.reversed() else listItems).forEach { v -> + v.text = (n++).toString() + } + outerContext.subOrdered(listItems.map { it.text }) + } "ul" -> outerContext.subUnordered() "dl" -> outerContext.subDefinition() "blockquote" -> outerContext.subQuote() @@ -881,7 +893,7 @@ object HTMLDecoder { EncodeSpanEnv( options = options, listContext = listContext, - tag = tag, + node = this, sb = sb, sbTmp = SpannableStringBuilder(), spanStart = 0, @@ -892,14 +904,14 @@ object HTMLDecoder { EncodeSpanEnv( options = options, listContext = listContext, - tag = tag, + node = this, sb = sb, sbTmp = sb, spanStart = sb.length ) } - val childListContext = childListContext(tag, listContext) + val childListContext = childListContext(this, listContext) child_nodes.forEachIndexed { i, child -> if (!canSkipEncode( diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 5c0bb1b5..7b1ada49 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -44,6 +44,8 @@ + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 347de51a..fa5daad1 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -36,7 +36,6 @@ - @color/Light_colorTextHelp @color/Light_colorLink @color/Light_colotListItemDrag @color/Light_colorColumnHeaderBg @@ -54,6 +53,9 @@ @color/Light_colorTextColumnListItem @color/Light_colorTextTimeSmall @color/Light_colorTextContent + @color/Light_colorTextHint + @color/Light_colorTextHelp + @color/Light_colorProfileBackgroundMask @color/Light_colorShowMediaBackground @color/Light_colorShowMediaText @@ -131,8 +133,6 @@ - @color/Dark_colorTextHelp - @color/Dark_colorLink @color/Dark_colorListItemDrag @color/Dark_colorColumnHeader @@ -151,6 +151,8 @@ @color/Dark_colorTextColumnListItem @color/Dark_colorTextTimeSmall @color/Dark_colorTextContent + @color/Dark_colorTextHint + @color/Dark_colorTextHelp @color/Dark_colorProfileBackgroundMask @color/Dark_colorShowMediaBackground @@ -233,7 +235,6 @@ - @color/Mastodon_colorTextHelp @color/Mastodon_colorLink @color/Mastodon_colorListItemDrag @@ -253,6 +254,8 @@ @color/Mastodon_colorTextColumnListItem @color/Mastodon_colorTextTimeSmall @color/Mastodon_colorTextContent + @color/Mastodon_colorTextHint + @color/Mastodon_colorTextHelp @color/Mastodon_colorProfileBackgroundMask @color/Mastodon_colorShowMediaBackground