This commit is contained in:
+31
@@ -1,8 +1,10 @@
|
|||||||
package com.m57.hermescontrol.ui.chat
|
package com.m57.hermescontrol.ui.chat
|
||||||
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
@@ -30,4 +32,33 @@ class MarkdownHeadingRenderingTest {
|
|||||||
composeTestRule.onNodeWithText("Set foo now").assertIsDisplayed()
|
composeTestRule.onNodeWithText("Set foo now").assertIsDisplayed()
|
||||||
composeTestRule.onNodeWithText("**Set `foo` now**").assertDoesNotExist()
|
composeTestRule.onNodeWithText("**Set `foo` now**").assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun quoteAndListContinuationRenderNestedCodeBlocks() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
MaterialTheme {
|
||||||
|
MarkdownText(
|
||||||
|
text =
|
||||||
|
"""
|
||||||
|
> # Quote heading
|
||||||
|
> ```kotlin
|
||||||
|
> val quoted = 1
|
||||||
|
> ```
|
||||||
|
|
||||||
|
- Item
|
||||||
|
```kotlin
|
||||||
|
val listed = 2
|
||||||
|
```
|
||||||
|
""".trimIndent(),
|
||||||
|
textColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Quote heading").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Item").assertIsDisplayed()
|
||||||
|
composeTestRule.onAllNodesWithTag("code_block").assertCountEquals(2)
|
||||||
|
composeTestRule.onNodeWithText("val quoted = 1", substring = true).assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("val listed = 2", substring = true).assertIsDisplayed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
package com.m57.hermescontrol.ui.chat
|
package com.m57.hermescontrol.ui.chat
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.getUnclippedBoundsInRoot
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -32,4 +40,26 @@ class MarkdownTableRenderingTest {
|
|||||||
composeTestRule.onNodeWithText("**Name**").assertDoesNotExist()
|
composeTestRule.onNodeWithText("**Name**").assertDoesNotExist()
|
||||||
composeTestRule.onNodeWithText("`code`").assertDoesNotExist()
|
composeTestRule.onNodeWithText("`code`").assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun columnsSizeToContentAndStayAlignedAcrossRows() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
MaterialTheme {
|
||||||
|
Box(Modifier.width(220.dp)) {
|
||||||
|
MarkdownText(
|
||||||
|
text = "| ID | Description |\n| --- | --- |\n| A | A considerably longer table value |",
|
||||||
|
textColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val idHeader = composeTestRule.onNodeWithTag("markdown_table_header_0").getUnclippedBoundsInRoot()
|
||||||
|
val descHeader = composeTestRule.onNodeWithTag("markdown_table_header_1").getUnclippedBoundsInRoot()
|
||||||
|
val idCell = composeTestRule.onNodeWithTag("markdown_table_cell_0_0").getUnclippedBoundsInRoot()
|
||||||
|
val descCell = composeTestRule.onNodeWithTag("markdown_table_cell_0_1").getUnclippedBoundsInRoot()
|
||||||
|
assertTrue(descHeader.right - descHeader.left > idHeader.right - idHeader.left)
|
||||||
|
assertEquals(idHeader.right - idHeader.left, idCell.right - idCell.left)
|
||||||
|
assertEquals(descHeader.right - descHeader.left, descCell.right - descCell.left)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,14 +152,14 @@ fun MarkdownText(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is MdBlock.Heading -> {
|
is MdBlock.Heading -> {
|
||||||
val fontSize =
|
val headingStyle =
|
||||||
when (block.level) {
|
when (block.level) {
|
||||||
1 -> 22.sp
|
1 -> MaterialTheme.typography.headlineMedium
|
||||||
2 -> 20.sp
|
2 -> MaterialTheme.typography.headlineSmall
|
||||||
3 -> 18.sp
|
3 -> MaterialTheme.typography.titleMedium
|
||||||
4 -> 16.sp
|
4 -> MaterialTheme.typography.titleSmall
|
||||||
5 -> 15.sp
|
5 -> MaterialTheme.typography.labelLarge
|
||||||
else -> 14.sp
|
else -> MaterialTheme.typography.labelMedium
|
||||||
}
|
}
|
||||||
val isRtl = remember(block.text) { BidiUtils.isRtlText(block.text) }
|
val isRtl = remember(block.text) { BidiUtils.isRtlText(block.text) }
|
||||||
val blockDirection = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
val blockDirection = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||||
@@ -169,9 +169,7 @@ fun MarkdownText(
|
|||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
latexMeasurer = latexMeasurer,
|
latexMeasurer = latexMeasurer,
|
||||||
style =
|
style =
|
||||||
MaterialTheme.typography.bodyMedium
|
headingStyle.copy(
|
||||||
.copy(
|
|
||||||
fontSize = fontSize,
|
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
textDirection = bidiTextDirection(isRtl),
|
textDirection = bidiTextDirection(isRtl),
|
||||||
),
|
),
|
||||||
@@ -209,6 +207,7 @@ fun MarkdownText(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.padding(end = 6.dp),
|
modifier = Modifier.padding(end = 6.dp),
|
||||||
)
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
MarkdownInlineText(
|
MarkdownInlineText(
|
||||||
text = block.text,
|
text = block.text,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
@@ -221,8 +220,17 @@ fun MarkdownText(
|
|||||||
isCurrentMatch = isCurrentMatch,
|
isCurrentMatch = isCurrentMatch,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
highlights = highlights,
|
highlights = highlights,
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
)
|
||||||
|
if (block.nestedSource.isNotEmpty()) {
|
||||||
|
MarkdownText(
|
||||||
|
text = block.nestedSource,
|
||||||
|
textColor = textColor,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isCurrentMatch = isCurrentMatch,
|
||||||
|
onImageClick = onImageClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,6 +266,7 @@ fun MarkdownText(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.size(18.dp).padding(top = 1.dp, end = 6.dp),
|
modifier = Modifier.size(18.dp).padding(top = 1.dp, end = 6.dp),
|
||||||
)
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
MarkdownInlineText(
|
MarkdownInlineText(
|
||||||
text = block.text,
|
text = block.text,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
@@ -270,8 +279,17 @@ fun MarkdownText(
|
|||||||
isCurrentMatch = isCurrentMatch,
|
isCurrentMatch = isCurrentMatch,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
highlights = highlights,
|
highlights = highlights,
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
)
|
||||||
|
if (block.nestedSource.isNotEmpty()) {
|
||||||
|
MarkdownText(
|
||||||
|
text = block.nestedSource,
|
||||||
|
textColor = textColor,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isCurrentMatch = isCurrentMatch,
|
||||||
|
onImageClick = onImageClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +313,7 @@ fun MarkdownText(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.padding(end = 6.dp),
|
modifier = Modifier.padding(end = 6.dp),
|
||||||
)
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
MarkdownInlineText(
|
MarkdownInlineText(
|
||||||
text = block.text,
|
text = block.text,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
@@ -307,8 +326,17 @@ fun MarkdownText(
|
|||||||
isCurrentMatch = isCurrentMatch,
|
isCurrentMatch = isCurrentMatch,
|
||||||
linkColor = linkColor,
|
linkColor = linkColor,
|
||||||
highlights = highlights,
|
highlights = highlights,
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
)
|
||||||
|
if (block.nestedSource.isNotEmpty()) {
|
||||||
|
MarkdownText(
|
||||||
|
text = block.nestedSource,
|
||||||
|
textColor = textColor,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isCurrentMatch = isCurrentMatch,
|
||||||
|
onImageClick = onImageClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,6 +358,8 @@ fun MarkdownText(
|
|||||||
.background(textColor.copy(alpha = 0.35f)),
|
.background(textColor.copy(alpha = 0.35f)),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
val nestedBlocks = remember(block.text) { parseBlocks(block.text) }
|
||||||
|
if (nestedBlocks.size == 1 && nestedBlocks.single() is MdBlock.Paragraph) {
|
||||||
MarkdownInlineText(
|
MarkdownInlineText(
|
||||||
text = block.text,
|
text = block.text,
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
@@ -345,6 +375,16 @@ fun MarkdownText(
|
|||||||
highlights = highlights,
|
highlights = highlights,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
MarkdownText(
|
||||||
|
text = block.text,
|
||||||
|
textColor = textColor,
|
||||||
|
searchQuery = searchQuery,
|
||||||
|
isCurrentMatch = isCurrentMatch,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onImageClick = onImageClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,18 +18,21 @@ sealed interface MdBlock {
|
|||||||
data class Bullet(
|
data class Bullet(
|
||||||
val text: String,
|
val text: String,
|
||||||
val level: Int = 0,
|
val level: Int = 0,
|
||||||
|
val nestedSource: String = "",
|
||||||
) : MdBlock
|
) : MdBlock
|
||||||
|
|
||||||
data class Task(
|
data class Task(
|
||||||
val checked: Boolean,
|
val checked: Boolean,
|
||||||
val text: String,
|
val text: String,
|
||||||
val level: Int = 0,
|
val level: Int = 0,
|
||||||
|
val nestedSource: String = "",
|
||||||
) : MdBlock
|
) : MdBlock
|
||||||
|
|
||||||
data class Ordered(
|
data class Ordered(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val text: String,
|
val text: String,
|
||||||
val level: Int = 0,
|
val level: Int = 0,
|
||||||
|
val nestedSource: String = "",
|
||||||
) : MdBlock
|
) : MdBlock
|
||||||
|
|
||||||
data class Image(
|
data class Image(
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ fun parseBlocks(src: String): List<MdBlock> {
|
|||||||
line.startsWith(">") -> {
|
line.startsWith(">") -> {
|
||||||
val quote = mutableListOf<String>()
|
val quote = mutableListOf<String>()
|
||||||
while (i < lines.size && lines[i].startsWith(">")) {
|
while (i < lines.size && lines[i].startsWith(">")) {
|
||||||
quote.add(lines[i].removePrefix(">").trim())
|
quote.add(lines[i].removePrefix(">").removePrefix(" "))
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
blocks.add(MdBlock.Quote(quote.joinToString("\n")))
|
blocks.add(MdBlock.Quote(quote.joinToString("\n")))
|
||||||
@@ -287,6 +287,11 @@ private fun parseList(
|
|||||||
}
|
}
|
||||||
if (lookahead < lines.size && LIST_ITEM_LINE_RE.matches(lines[lookahead])) {
|
if (lookahead < lines.size && LIST_ITEM_LINE_RE.matches(lines[lookahead])) {
|
||||||
j = lookahead
|
j = lookahead
|
||||||
|
} else if (items.isNotEmpty() && lookahead < lines.size &&
|
||||||
|
computeIndent(lines[lookahead].takeWhile { it == ' ' || it == '\t' }) >= 2
|
||||||
|
) {
|
||||||
|
items.last().continuationLines.add("")
|
||||||
|
j = lookahead
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -334,32 +339,34 @@ private fun parseList(
|
|||||||
level = indentStack.size - 1
|
level = indentStack.size - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
val fullText =
|
|
||||||
if (item.continuationLines.isEmpty()) {
|
|
||||||
when (item) {
|
|
||||||
is ParsedItem.Bullet -> item.text
|
|
||||||
is ParsedItem.Task -> item.text
|
|
||||||
is ParsedItem.Ordered -> item.text
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val base =
|
val base =
|
||||||
when (item) {
|
when (item) {
|
||||||
is ParsedItem.Bullet -> item.text
|
is ParsedItem.Bullet -> item.text
|
||||||
is ParsedItem.Task -> item.text
|
is ParsedItem.Task -> item.text
|
||||||
is ParsedItem.Ordered -> item.text
|
is ParsedItem.Ordered -> item.text
|
||||||
}
|
}
|
||||||
base + " " + item.continuationLines.joinToString(" ")
|
val nestedSource =
|
||||||
|
if (item.continuationLines.any(::startsNestedBlock)) {
|
||||||
|
item.continuationLines.joinToString("\n")
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
val fullText =
|
||||||
|
if (nestedSource.isNotEmpty()) {
|
||||||
|
base
|
||||||
|
} else {
|
||||||
|
(listOf(base) + item.continuationLines).joinToString(" ").trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (item) {
|
when (item) {
|
||||||
is ParsedItem.Bullet -> {
|
is ParsedItem.Bullet -> {
|
||||||
blocks.add(MdBlock.Bullet(fullText, level = level))
|
blocks.add(MdBlock.Bullet(fullText, level = level, nestedSource = nestedSource))
|
||||||
lastItemWasOrdered = false
|
lastItemWasOrdered = false
|
||||||
lastLevel = level
|
lastLevel = level
|
||||||
}
|
}
|
||||||
|
|
||||||
is ParsedItem.Task -> {
|
is ParsedItem.Task -> {
|
||||||
blocks.add(MdBlock.Task(item.checked, fullText, level = level))
|
blocks.add(MdBlock.Task(item.checked, fullText, level = level, nestedSource = nestedSource))
|
||||||
lastItemWasOrdered = false
|
lastItemWasOrdered = false
|
||||||
lastLevel = level
|
lastLevel = level
|
||||||
}
|
}
|
||||||
@@ -377,7 +384,7 @@ private fun parseList(
|
|||||||
currentSequenceNumber = item.rawNumber
|
currentSequenceNumber = item.rawNumber
|
||||||
currentSequenceNumber
|
currentSequenceNumber
|
||||||
}
|
}
|
||||||
blocks.add(MdBlock.Ordered(indexToUse, fullText, level = level))
|
blocks.add(MdBlock.Ordered(indexToUse, fullText, level = level, nestedSource = nestedSource))
|
||||||
lastItemWasOrdered = true
|
lastItemWasOrdered = true
|
||||||
lastLevel = level
|
lastLevel = level
|
||||||
}
|
}
|
||||||
@@ -387,6 +394,15 @@ private fun parseList(
|
|||||||
return blocks to j
|
return blocks to j
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Preserve block syntax in indented list continuations instead of flattening it into prose. */
|
||||||
|
private fun startsNestedBlock(line: String): Boolean =
|
||||||
|
parseCodeFenceStart(line) != null ||
|
||||||
|
isValidHeading(line) ||
|
||||||
|
line.startsWith(">") ||
|
||||||
|
isHorizontalRule(line) ||
|
||||||
|
line.startsWith("$$") ||
|
||||||
|
line.startsWith("\\[")
|
||||||
|
|
||||||
private fun isHorizontalRule(line: String): Boolean {
|
private fun isHorizontalRule(line: String): Boolean {
|
||||||
val trimmed = line.trim()
|
val trimmed = line.trim()
|
||||||
if (trimmed.length < 3) return false
|
if (trimmed.length < 3) return false
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -25,8 +28,6 @@ import com.hrm.latex.renderer.measure.LatexMeasurerState
|
|||||||
import com.m57.hermescontrol.theme.SearchHighlightColors
|
import com.m57.hermescontrol.theme.SearchHighlightColors
|
||||||
import com.m57.hermescontrol.util.BidiUtils
|
import com.m57.hermescontrol.util.BidiUtils
|
||||||
|
|
||||||
private val TABLE_COL_WIDTH = 140.dp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownTable(
|
fun MarkdownTable(
|
||||||
block: MdBlock.Table,
|
block: MdBlock.Table,
|
||||||
@@ -46,6 +47,25 @@ fun MarkdownTable(
|
|||||||
val tableDirection = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
val tableDirection = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||||
val headerBg = textColor.copy(alpha = 0.08f)
|
val headerBg = textColor.copy(alpha = 0.08f)
|
||||||
val alignments = block.alignments
|
val alignments = block.alignments
|
||||||
|
val cellStyle = MaterialTheme.typography.bodySmall
|
||||||
|
val textMeasurer = rememberTextMeasurer()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val columnWidths =
|
||||||
|
remember(block, cellStyle, density, textMeasurer) {
|
||||||
|
val padding = with(density) { 12.dp.roundToPx() }
|
||||||
|
val minimum = with(density) { 48.dp.roundToPx() }
|
||||||
|
val maximum = with(density) { 280.dp.roundToPx() }
|
||||||
|
val columnCount = maxOf(block.header.size, block.rows.maxOfOrNull { it.size } ?: 0)
|
||||||
|
List(columnCount) { column ->
|
||||||
|
val cells =
|
||||||
|
listOfNotNull(block.header.getOrNull(column)) + block.rows.mapNotNull { it.getOrNull(column) }
|
||||||
|
val contentWidth =
|
||||||
|
cells.maxOfOrNull { cell ->
|
||||||
|
textMeasurer.measure(text = cell.take(128), style = cellStyle, maxLines = 1).size.width
|
||||||
|
} ?: 0
|
||||||
|
with(density) { (contentWidth + padding).coerceIn(minimum, maximum).toDp() }
|
||||||
|
}
|
||||||
|
}
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides tableDirection) {
|
CompositionLocalProvider(LocalLayoutDirection provides tableDirection) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -72,14 +92,15 @@ fun MarkdownTable(
|
|||||||
highlights = highlights,
|
highlights = highlights,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.width(TABLE_COL_WIDTH)
|
.width(columnWidths[idx])
|
||||||
|
.testTag("markdown_table_header_$idx")
|
||||||
.padding(6.dp),
|
.padding(6.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider(color = textColor.copy(alpha = 0.25f))
|
HorizontalDivider(color = textColor.copy(alpha = 0.25f))
|
||||||
// Body rows
|
// Body rows
|
||||||
block.rows.forEach { row ->
|
block.rows.forEachIndexed { rowIndex, row ->
|
||||||
Row(verticalAlignment = Alignment.Top) {
|
Row(verticalAlignment = Alignment.Top) {
|
||||||
row.forEachIndexed { idx, cell ->
|
row.forEachIndexed { idx, cell ->
|
||||||
MarkdownInlineText(
|
MarkdownInlineText(
|
||||||
@@ -96,7 +117,8 @@ fun MarkdownTable(
|
|||||||
highlights = highlights,
|
highlights = highlights,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.width(TABLE_COL_WIDTH)
|
.width(columnWidths[idx])
|
||||||
|
.testTag("markdown_table_cell_${rowIndex}_$idx")
|
||||||
.padding(6.dp),
|
.padding(6.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -459,6 +459,28 @@ class MarkdownTextFeatureTest {
|
|||||||
assertEquals(0, o16.level)
|
assertEquals(0, o16.level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun quotedBlocksRetainNestedHeadingsListsAndFences() {
|
||||||
|
val source = "> # Heading\n> - item\n> ```kotlin\n> val x = 1\n> ```"
|
||||||
|
val quote = parseBlocks(source).single() as MdBlock.Quote
|
||||||
|
val nested = parseBlocks(quote.text)
|
||||||
|
assertTrue(nested.any { it is MdBlock.Heading && it.text == "Heading" })
|
||||||
|
val bullet = nested.filterIsInstance<MdBlock.Bullet>().single()
|
||||||
|
assertEquals("item", bullet.text)
|
||||||
|
assertEquals("val x = 1", (parseBlocks(bullet.nestedSource).single() as MdBlock.Code).code)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun listItemsRetainFencedCodeAndHeadingsAsNestedBlocks() {
|
||||||
|
val source = "- intro\n ```kotlin\n val x = 1\n ```\n # Inside\n- next"
|
||||||
|
val bullets = parseBlocks(source).filterIsInstance<MdBlock.Bullet>()
|
||||||
|
assertEquals(listOf("intro", "next"), bullets.map { it.text })
|
||||||
|
val nested = parseBlocks(bullets.first().nestedSource)
|
||||||
|
assertTrue(nested.any { it is MdBlock.Code && it.code == "val x = 1" })
|
||||||
|
assertTrue(nested.any { it is MdBlock.Heading && it.text == "Inside" })
|
||||||
|
assertEquals("", bullets.last().nestedSource)
|
||||||
|
}
|
||||||
|
|
||||||
// 19. NESTED CODEBLOCKS & EXTENDED CODE FENCES
|
// 19. NESTED CODEBLOCKS & EXTENDED CODE FENCES
|
||||||
@Test
|
@Test
|
||||||
fun testNestedCodeBlock_fourBackticksWrappingThreeBackticks() {
|
fun testNestedCodeBlock_fourBackticksWrappingThreeBackticks() {
|
||||||
|
|||||||
Reference in New Issue
Block a user