This commit is contained in:
7 files changed
+257
-93
No files matched your search
@@ -152,14 +152,14 @@ fun MarkdownText(
|
||||
}
|
||||
|
||||
is MdBlock.Heading -> {
|
||||
val fontSize =
|
||||
val headingStyle =
|
||||
when (block.level) {
|
||||
1 -> 22.sp
|
||||
2 -> 20.sp
|
||||
3 -> 18.sp
|
||||
4 -> 16.sp
|
||||
5 -> 15.sp
|
||||
else -> 14.sp
|
||||
1 -> MaterialTheme.typography.headlineMedium
|
||||
2 -> MaterialTheme.typography.headlineSmall
|
||||
3 -> MaterialTheme.typography.titleMedium
|
||||
4 -> MaterialTheme.typography.titleSmall
|
||||
5 -> MaterialTheme.typography.labelLarge
|
||||
else -> MaterialTheme.typography.labelMedium
|
||||
}
|
||||
val isRtl = remember(block.text) { BidiUtils.isRtlText(block.text) }
|
||||
val blockDirection = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
@@ -169,12 +169,10 @@ fun MarkdownText(
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium
|
||||
.copy(
|
||||
fontSize = fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
headingStyle.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
@@ -209,20 +207,30 @@ fun MarkdownText(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(end = 6.dp),
|
||||
)
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
)
|
||||
if (block.nestedSource.isNotEmpty()) {
|
||||
MarkdownText(
|
||||
text = block.nestedSource,
|
||||
textColor = textColor,
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
onImageClick = onImageClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,20 +266,30 @@ fun MarkdownText(
|
||||
},
|
||||
modifier = Modifier.size(18.dp).padding(top = 1.dp, end = 6.dp),
|
||||
)
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
)
|
||||
if (block.nestedSource.isNotEmpty()) {
|
||||
MarkdownText(
|
||||
text = block.nestedSource,
|
||||
textColor = textColor,
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
onImageClick = onImageClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,20 +313,30 @@ fun MarkdownText(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(end = 6.dp),
|
||||
)
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
)
|
||||
if (block.nestedSource.isNotEmpty()) {
|
||||
MarkdownText(
|
||||
text = block.nestedSource,
|
||||
textColor = textColor,
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
onImageClick = onImageClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,21 +358,33 @@ fun MarkdownText(
|
||||
.background(textColor.copy(alpha = 0.35f)),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontStyle = FontStyle.Italic,
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
val nestedBlocks = remember(block.text) { parseBlocks(block.text) }
|
||||
if (nestedBlocks.size == 1 && nestedBlocks.single() is MdBlock.Paragraph) {
|
||||
MarkdownInlineText(
|
||||
text = block.text,
|
||||
textColor = textColor,
|
||||
latexMeasurer = latexMeasurer,
|
||||
style =
|
||||
MaterialTheme.typography.bodyMedium.copy(
|
||||
fontStyle = FontStyle.Italic,
|
||||
textDirection = bidiTextDirection(isRtl),
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
isCurrentMatch = isCurrentMatch,
|
||||
linkColor = linkColor,
|
||||
highlights = highlights,
|
||||
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(
|
||||
val text: String,
|
||||
val level: Int = 0,
|
||||
val nestedSource: String = "",
|
||||
) : MdBlock
|
||||
|
||||
data class Task(
|
||||
val checked: Boolean,
|
||||
val text: String,
|
||||
val level: Int = 0,
|
||||
val nestedSource: String = "",
|
||||
) : MdBlock
|
||||
|
||||
data class Ordered(
|
||||
val index: Int,
|
||||
val text: String,
|
||||
val level: Int = 0,
|
||||
val nestedSource: String = "",
|
||||
) : MdBlock
|
||||
|
||||
data class Image(
|
||||
|
||||
@@ -164,7 +164,7 @@ fun parseBlocks(src: String): List<MdBlock> {
|
||||
line.startsWith(">") -> {
|
||||
val quote = mutableListOf<String>()
|
||||
while (i < lines.size && lines[i].startsWith(">")) {
|
||||
quote.add(lines[i].removePrefix(">").trim())
|
||||
quote.add(lines[i].removePrefix(">").removePrefix(" "))
|
||||
i++
|
||||
}
|
||||
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])) {
|
||||
j = lookahead
|
||||
} else if (items.isNotEmpty() && lookahead < lines.size &&
|
||||
computeIndent(lines[lookahead].takeWhile { it == ' ' || it == '\t' }) >= 2
|
||||
) {
|
||||
items.last().continuationLines.add("")
|
||||
j = lookahead
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -334,32 +339,34 @@ private fun parseList(
|
||||
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
|
||||
}
|
||||
val base =
|
||||
when (item) {
|
||||
is ParsedItem.Bullet -> item.text
|
||||
is ParsedItem.Task -> item.text
|
||||
is ParsedItem.Ordered -> item.text
|
||||
}
|
||||
val nestedSource =
|
||||
if (item.continuationLines.any(::startsNestedBlock)) {
|
||||
item.continuationLines.joinToString("\n")
|
||||
} else {
|
||||
val base =
|
||||
when (item) {
|
||||
is ParsedItem.Bullet -> item.text
|
||||
is ParsedItem.Task -> item.text
|
||||
is ParsedItem.Ordered -> item.text
|
||||
}
|
||||
base + " " + item.continuationLines.joinToString(" ")
|
||||
""
|
||||
}
|
||||
val fullText =
|
||||
if (nestedSource.isNotEmpty()) {
|
||||
base
|
||||
} else {
|
||||
(listOf(base) + item.continuationLines).joinToString(" ").trim()
|
||||
}
|
||||
|
||||
when (item) {
|
||||
is ParsedItem.Bullet -> {
|
||||
blocks.add(MdBlock.Bullet(fullText, level = level))
|
||||
blocks.add(MdBlock.Bullet(fullText, level = level, nestedSource = nestedSource))
|
||||
lastItemWasOrdered = false
|
||||
lastLevel = level
|
||||
}
|
||||
|
||||
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
|
||||
lastLevel = level
|
||||
}
|
||||
@@ -377,7 +384,7 @@ private fun parseList(
|
||||
currentSequenceNumber = item.rawNumber
|
||||
currentSequenceNumber
|
||||
}
|
||||
blocks.add(MdBlock.Ordered(indexToUse, fullText, level = level))
|
||||
blocks.add(MdBlock.Ordered(indexToUse, fullText, level = level, nestedSource = nestedSource))
|
||||
lastItemWasOrdered = true
|
||||
lastLevel = level
|
||||
}
|
||||
@@ -387,6 +394,15 @@ private fun parseList(
|
||||
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 {
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.length < 3) return false
|
||||
|
||||
@@ -16,8 +16,11 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
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.util.BidiUtils
|
||||
|
||||
private val TABLE_COL_WIDTH = 140.dp
|
||||
|
||||
@Composable
|
||||
fun MarkdownTable(
|
||||
block: MdBlock.Table,
|
||||
@@ -46,6 +47,25 @@ fun MarkdownTable(
|
||||
val tableDirection = if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
val headerBg = textColor.copy(alpha = 0.08f)
|
||||
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) {
|
||||
Column(
|
||||
modifier =
|
||||
@@ -72,14 +92,15 @@ fun MarkdownTable(
|
||||
highlights = highlights,
|
||||
modifier =
|
||||
Modifier
|
||||
.width(TABLE_COL_WIDTH)
|
||||
.width(columnWidths[idx])
|
||||
.testTag("markdown_table_header_$idx")
|
||||
.padding(6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = textColor.copy(alpha = 0.25f))
|
||||
// Body rows
|
||||
block.rows.forEach { row ->
|
||||
block.rows.forEachIndexed { rowIndex, row ->
|
||||
Row(verticalAlignment = Alignment.Top) {
|
||||
row.forEachIndexed { idx, cell ->
|
||||
MarkdownInlineText(
|
||||
@@ -96,7 +117,8 @@ fun MarkdownTable(
|
||||
highlights = highlights,
|
||||
modifier =
|
||||
Modifier
|
||||
.width(TABLE_COL_WIDTH)
|
||||
.width(columnWidths[idx])
|
||||
.testTag("markdown_table_cell_${rowIndex}_$idx")
|
||||
.padding(6.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user