@@ -164,6 +164,7 @@ fun MainScreen(viewModel: MainViewModel) {
164164 modifier = Modifier
165165 .fillMaxWidth()
166166 .weight(1f , fill = false )
167+ .animateContentSize()
167168 .clip(RoundedCornerShape (16 .dp))
168169 .background(MaterialTheme .colorScheme.surfaceContainerHigh)
169170 .padding(16 .dp),
@@ -1191,50 +1192,85 @@ fun NoRootScreen() {
11911192
11921193@Composable
11931194fun formatChangelog (text : String , linkColor : Color ): AnnotatedString {
1194- val titleLargeStyle = MaterialTheme .typography.titleLarge.toSpanStyle().copy(fontWeight = FontWeight .Bold )
1195- val titleMediumStyle = MaterialTheme .typography.titleMedium.toSpanStyle().copy(fontWeight = FontWeight .Bold )
1195+ val titleLargeStyle = MaterialTheme .typography.titleLarge.toSpanStyle().copy(
1196+ fontWeight = FontWeight .ExtraBold ,
1197+ color = MaterialTheme .colorScheme.primary
1198+ )
1199+ val titleMediumStyle = MaterialTheme .typography.titleMedium.toSpanStyle().copy(
1200+ fontWeight = FontWeight .Bold ,
1201+ color = MaterialTheme .colorScheme.primary
1202+ )
1203+ val codeStyle = SpanStyle (
1204+ fontFamily = androidx.compose.ui.text.font.FontFamily .Monospace ,
1205+ background = MaterialTheme .colorScheme.surfaceVariant,
1206+ color = MaterialTheme .colorScheme.onSurfaceVariant
1207+ )
1208+ val quoteStyle = SpanStyle (
1209+ color = MaterialTheme .colorScheme.onSurfaceVariant.copy(alpha = 0.7f ),
1210+ fontStyle = androidx.compose.ui.text.font.FontStyle .Italic
1211+ )
11961212
11971213 return buildAnnotatedString {
11981214 val lines = text.split(" \n " )
11991215
1200- for ((lineIndex , line) in lines.withIndex()) {
1216+ lines.forEachIndexed { index , line ->
12011217 var currentLine = line
1202- var headingStyle: SpanStyle ? = null
1203-
1204- if (currentLine.startsWith(" ### " )) {
1205- currentLine = currentLine.removePrefix(" ### " )
1206- headingStyle = titleMediumStyle
1207- } else if (currentLine.startsWith(" ## " )) {
1208- currentLine = currentLine.removePrefix(" ## " )
1209- headingStyle = titleLargeStyle
1210- } else if (currentLine.startsWith(" # " )) {
1211- currentLine = currentLine.removePrefix(" # " )
1212- headingStyle = titleLargeStyle
1213- }
1218+ var lineStyle: SpanStyle ? = null
1219+ var prefix = " "
1220+
1221+ if (currentLine.matches(Regex (" ^-{3,}$|^\\ *{3,}$|^_{3,}$" ))) {
1222+ withStyle(SpanStyle (color = MaterialTheme .colorScheme.outlineVariant)) {
1223+ append(" ────────────────────────────────" )
1224+ }
1225+ } else {
1226+ if (currentLine.startsWith(" ### " )) {
1227+ currentLine = currentLine.removePrefix(" ### " )
1228+ lineStyle = titleMediumStyle
1229+ } else if (currentLine.startsWith(" ## " )) {
1230+ currentLine = currentLine.removePrefix(" ## " )
1231+ lineStyle = titleLargeStyle
1232+ } else if (currentLine.startsWith(" # " )) {
1233+ currentLine = currentLine.removePrefix(" # " )
1234+ lineStyle = titleLargeStyle
1235+ }
12141236
1215- val lineStart = length
1237+ if (currentLine.startsWith(" > " )) {
1238+ currentLine = currentLine.removePrefix(" > " )
1239+ lineStyle = quoteStyle
1240+ prefix = " ┃ "
1241+ }
12161242
1217- val parts = currentLine.split(" **" )
1218- for ((index, part) in parts.withIndex()) {
1219- if (index % 2 == 1 ) {
1220- withStyle(SpanStyle (fontWeight = FontWeight .Bold )) { append(part) }
1221- } else {
1222- append(part)
1243+ if (currentLine.trimStart().startsWith(" - " ) || currentLine.trimStart()
1244+ .startsWith(" * " )
1245+ ) {
1246+ val indent = currentLine.takeWhile { it.isWhitespace() }
1247+ currentLine = currentLine.trimStart().substring(2 )
1248+ prefix = " $indent • "
1249+ }
1250+
1251+ val lineStart = length
1252+ append(prefix)
1253+
1254+ parseInlineStyles(currentLine, codeStyle).forEach { part ->
1255+ val partStart = length
1256+ append(part.text)
1257+ part.styles.forEach { (style, range) ->
1258+ addStyle(style, partStart + range.first, partStart + range.last)
1259+ }
12231260 }
1224- }
12251261
1226- if (headingStyle != null ) {
1227- addStyle(headingStyle, lineStart, length)
1262+ if (lineStyle != null ) {
1263+ addStyle(lineStyle, lineStart, length)
1264+ }
12281265 }
12291266
1230- if (lineIndex < lines.size - 1 ) {
1267+ if (index < lines.size - 1 ) {
12311268 append(" \n " )
12321269 }
12331270 }
12341271
1235- val finalStr = toAnnotatedString().text
12361272 val urlRegex = Regex (" (https?://\\ S+)" )
1237- urlRegex.findAll(finalStr ).forEach { match ->
1273+ urlRegex.findAll(this .toAnnotatedString().text ).forEach { match ->
12381274 addLink(
12391275 LinkAnnotation .Url (
12401276 url = match.value,
@@ -1250,4 +1286,95 @@ fun formatChangelog(text: String, linkColor: Color): AnnotatedString {
12501286 )
12511287 }
12521288 }
1253- }
1289+ }
1290+
1291+ private data class StyledPart (val text : String , val styles : List <Pair <SpanStyle , IntRange >>)
1292+
1293+ private fun parseInlineStyles (text : String , codeStyle : SpanStyle ): List <StyledPart > {
1294+ val result = mutableListOf<StyledPart >()
1295+
1296+ val codeRegex = Regex (" `([^`]+)`" )
1297+ var lastMatchEnd = 0
1298+
1299+ codeRegex.findAll(text).forEach { match ->
1300+ if (match.range.first > lastMatchEnd) {
1301+ result.add(parseBoldItalic(text.substring(lastMatchEnd, match.range.first)))
1302+ }
1303+ result.add(
1304+ StyledPart (
1305+ match.groupValues[1 ],
1306+ listOf (codeStyle to 0 .rangeTo(match.groupValues[1 ].length))
1307+ )
1308+ )
1309+ lastMatchEnd = match.range.last + 1
1310+ }
1311+
1312+ if (lastMatchEnd < text.length) {
1313+ result.add(parseBoldItalic(text.substring(lastMatchEnd)))
1314+ }
1315+
1316+ return if (result.isEmpty()) listOf (parseBoldItalic(text)) else result
1317+ }
1318+
1319+ private fun parseBoldItalic (text : String ): StyledPart {
1320+ val styles = mutableListOf<Pair <SpanStyle , IntRange >>()
1321+
1322+ val boldRegex = Regex (" (\\ *\\ *|__)(.*?)\\ 1" )
1323+ boldRegex.findAll(text).forEach { match ->
1324+ styles.add(SpanStyle (fontWeight = FontWeight .Bold ) to match.range.first.rangeTo(match.range.last + 1 ))
1325+ }
1326+
1327+ val italicRegex =
1328+ Regex (" (?<!\\ *)\\ *(?!\\ *)(.*?)(?<!\\ *)\\ *(?!\\ *)|(?<!_)_(?!_)(.*?)(?<!_)_(?!_)" )
1329+ italicRegex.findAll(text).forEach { match ->
1330+ styles.add(
1331+ SpanStyle (fontStyle = androidx.compose.ui.text.font.FontStyle .Italic ) to match.range.first.rangeTo(
1332+ match.range.last + 1
1333+ )
1334+ )
1335+ }
1336+
1337+
1338+ val finalBuilder = AnnotatedString .Builder ()
1339+ var i = 0
1340+ while (i < text.length) {
1341+ when {
1342+ text.startsWith(" **" , i) || text.startsWith(" __" , i) -> {
1343+ val marker = text.substring(i, i + 2 )
1344+ val end = text.indexOf(marker, i + 2 )
1345+ if (end != - 1 ) {
1346+ finalBuilder.withStyle(SpanStyle (fontWeight = FontWeight .Bold )) {
1347+ append(text.substring(i + 2 , end))
1348+ }
1349+ i = end + 2
1350+ } else {
1351+ finalBuilder.append(text[i])
1352+ i++
1353+ }
1354+ }
1355+
1356+ text.startsWith(" *" , i) || text.startsWith(" _" , i) -> {
1357+ val marker = text[i].toString()
1358+ val end = text.indexOf(marker, i + 1 )
1359+ if (end != - 1 && (end + 1 == text.length || text[end + 1 ] != text[end])) {
1360+ finalBuilder.withStyle(SpanStyle (fontStyle = androidx.compose.ui.text.font.FontStyle .Italic )) {
1361+ append(text.substring(i + 1 , end))
1362+ }
1363+ i = end + 1
1364+ } else {
1365+ finalBuilder.append(text[i])
1366+ i++
1367+ }
1368+ }
1369+
1370+ else -> {
1371+ finalBuilder.append(text[i])
1372+ i++
1373+ }
1374+ }
1375+ }
1376+
1377+ return StyledPart (
1378+ finalBuilder.toAnnotatedString().text,
1379+ finalBuilder.toAnnotatedString().spanStyles.map { it.item to it.start.rangeTo(it.end) })
1380+ }
0 commit comments