feat: use an actual markdown rendering library

it even highlights code blocks
This commit is contained in:
Infi 2023-03-12 04:35:34 +01:00
parent 5116357e37
commit a43f68b33e
27 changed files with 580 additions and 296 deletions

View File

@ -13,7 +13,6 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/markdown" />
</set>
</option>
</GradleProjectSettings>

View File

@ -13,7 +13,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -163,7 +163,8 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
// Markdown
implementation project(':markdown')
implementation "com.github.discord:SimpleAST:2.7.0"
implementation "androidx.appcompat:appcompat:1.7.0-alpha02"
}
kapt {

View File

@ -1,6 +1,8 @@
package chat.revolt.components.chat
import android.net.Uri
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.*
@ -9,11 +11,15 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.ResourcesCompat
import chat.revolt.R
import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.RevoltAPI
import chat.revolt.api.asJanuaryProxyUrl
@ -22,7 +28,6 @@ import chat.revolt.api.internals.WebCompat
import chat.revolt.api.schemas.AutumnResource
import chat.revolt.components.generic.UserAvatar
import chat.revolt.components.generic.UserAvatarWidthPlaceholder
import chat.revolt.markdown.Markdown
import chat.revolt.api.schemas.Message as MessageSchema
fun viewAttachmentInBrowser(ctx: android.content.Context, attachment: AutumnResource) {
@ -49,10 +54,12 @@ fun formatLongAsTime(time: Long): String {
fun Message(
message: MessageSchema,
truncate: Boolean = false,
parse: (MessageSchema) -> SpannableStringBuilder = { SpannableStringBuilder(it.content) },
onMessageContextMenu: () -> Unit = {},
) {
val author = RevoltAPI.userCache[message.author] ?: return CircularProgressIndicator()
val context = LocalContext.current
val contentColor = LocalContentColor.current
Column {
if (message.tail == false) {
@ -134,11 +141,17 @@ fun Message(
message.content?.let {
if (message.content.isBlank()) return@let // if only an attachment is sent
Text(
text = Markdown.annotate(it),
maxLines = if (truncate) 1 else Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis
)
AndroidView(factory = { ctx ->
androidx.appcompat.widget.AppCompatTextView(ctx).apply {
text = parse(message)
maxLines = if (truncate) 1 else Int.MAX_VALUE
ellipsize = TextUtils.TruncateAt.END
textSize = 16f
typeface = ResourcesCompat.getFont(ctx, R.font.inter)
setTextColor(contentColor.toArgb())
}
})
}
message.attachments?.let {

View File

@ -0,0 +1,118 @@
package chat.revolt.components.generic
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.util.Log
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.ResourcesCompat
import chat.revolt.R
import chat.revolt.api.RevoltAPI
import chat.revolt.internals.markdown.ChannelMentionRule
import chat.revolt.internals.markdown.CustomEmoteRule
import chat.revolt.internals.markdown.MarkdownContext
import chat.revolt.internals.markdown.MarkdownParser
import chat.revolt.internals.markdown.MarkdownState
import chat.revolt.internals.markdown.UserMentionRule
import chat.revolt.internals.markdown.createCodeRule
import chat.revolt.internals.markdown.createInlineCodeRule
import com.discord.simpleast.core.simple.SimpleMarkdownRules
import com.discord.simpleast.core.simple.SimpleRenderer
/**
* A Markdown rendering component for Markdown embedded in UI (e.g. in a button).
* @param text The text to render.
* @param fontSize The font size to use.
* @param modifier The modifier to apply to the rendered text. Will be applied to AndroidView and thus subject to AndroidView's limitations.
* @param maxLines The maximum number of lines to display. Text will always be ellipsized on overflow. Defaults to [Int.MAX_VALUE].
*/
@Composable
fun UIMarkdown(
text: String,
fontSize: TextUnit,
modifier: Modifier = Modifier,
maxLines: Int = Int.MAX_VALUE,
) {
val context = LocalContext.current
val foregroundColor = LocalContentColor.current
val codeBlockColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
val spannableStringBuilder = remember { mutableStateOf(SpannableStringBuilder()) }
LaunchedEffect(text) {
val parser = MarkdownParser()
.addRules(
SimpleMarkdownRules.createEscapeRule(),
UserMentionRule(),
ChannelMentionRule(),
CustomEmoteRule(),
)
.addRules(
createCodeRule(context, codeBlockColor.toArgb()),
createInlineCodeRule(context, codeBlockColor.toArgb()),
)
.addRules(
SimpleMarkdownRules.createSimpleMarkdownRules(
includeEscapeRule = false
)
)
spannableStringBuilder.value = SimpleRenderer.render(
source = text,
parser = parser,
initialState = MarkdownState(0),
renderContext = MarkdownContext(
memberMap = mapOf(),
userMap = RevoltAPI.userCache.toMap(),
channelMap = RevoltAPI.channelCache.mapValues { ch ->
ch.value.name ?: ch.value.id!!
},
emojiMap = RevoltAPI.emojiCache,
serverId = null
)
)
Log.d("Markdown", "Rendered: ${spannableStringBuilder.value}")
}
AndroidView(
factory = {
androidx.appcompat.widget.AppCompatTextView(it).apply {
ellipsize = TextUtils.TruncateAt.END
typeface = ResourcesCompat.getFont(it, R.font.inter)
setTextColor(foregroundColor.toArgb())
setMaxLines(maxLines)
setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, fontSize.value)
setText(spannableStringBuilder.value)
}
},
modifier = modifier,
update = {
it.text = spannableStringBuilder.value
},
)
}
@Preview
@Composable
fun UIMarkdownPreview() {
// Will not render in side preview but will render on device
UIMarkdown(
text = "Hello, **world**!",
fontSize = 16.sp,
)
}

View File

@ -0,0 +1,129 @@
package chat.revolt.internals.markdown
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.LeadingMarginSpan
import android.text.style.LineBackgroundSpan
import androidx.annotation.ColorInt
import com.discord.simpleast.core.node.Node
// Attribution:
// https://github.com/discord/SimpleAST/blob/567b61c51056cbdec39e839100690c576c26a4c6/app/src/main/java/com/discord/simpleast/sample/spans/BlockBackgroundNode.kt
// LICENSED UNDER THE APACHE LICENSE, VERSION 2.0
// Adapted for Revolt.
/**
* Creates a block background for code sections.
*/
class BlockBackgroundNode<R>(
private val quoteDepth: Int,
private val fillColor: Int = Color.DKGRAY,
private val strokeColor: Int = Color.BLACK,
vararg children: Node<R>,
) : Node.Parent<R>(*children) {
override fun render(builder: SpannableStringBuilder, renderContext: R) {
// Ensure the block we want to append starts on a newline.
ensureEndsWithNewline(builder)
val codeStartIndex = builder.length
super.render(builder, renderContext)
// BlockBackgroundSpan requires this to function
ensureEndsWithNewline(builder)
val backgroundSpan = BlockBackgroundSpan(
fillColor, strokeColor,
strokeWidth = 2,
strokeRadius = 15,
leftMargin = 40 * quoteDepth
)
builder.setSpan(
backgroundSpan,
codeStartIndex,
builder.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
// Apply a leading margin to all lines in the block.
val leadingMarginSpan = LeadingMarginSpan.Standard(15)
builder.setSpan(
leadingMarginSpan,
codeStartIndex,
builder.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
private fun ensureEndsWithNewline(builder: SpannableStringBuilder) {
if (builder.isNotEmpty()) {
val lastChar = CharArray(6)
builder.getChars(builder.length - 1, builder.length, lastChar, 0)
if (lastChar[0] != '\n') {
builder.append('\n')
}
}
}
}
/**
* Computes the position of the paragraph on the screen and draws the desired background.
*/
class BlockBackgroundSpan(
@ColorInt fillColor: Int,
@ColorInt strokeColor: Int,
strokeWidth: Int,
strokeRadius: Int,
val leftMargin: Int
) : LineBackgroundSpan {
private val fillPaint = Paint().apply {
this.style = Paint.Style.FILL
this.color = fillColor
}
private val strokePaint = Paint().apply {
this.style = Paint.Style.STROKE
this.color = strokeColor
this.strokeWidth = strokeWidth.toFloat()
this.isAntiAlias = true
}
private val rect = RectF()
private val radius = strokeRadius.toFloat()
fun draw(canvas: Canvas) {
canvas.drawRoundRect(rect, radius, radius, fillPaint)
canvas.drawRoundRect(rect, radius, radius, strokePaint)
}
override fun drawBackground(
canvas: Canvas,
paint: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lnum: Int
) {
if (text !is Spanned) return
if (text.getSpanStart(this) == start) {
rect.left = left.toFloat() + leftMargin
rect.top = top.toFloat()
}
if (text.getSpanEnd(this) == end) {
rect.right = right.toFloat()
rect.bottom = bottom.toFloat()
draw(canvas)
}
}
}

View File

@ -1,11 +1,20 @@
package chat.revolt.internals.markdown
import androidx.compose.runtime.snapshots.SnapshotStateMap
import chat.revolt.api.schemas.Emoji
import chat.revolt.api.schemas.User
import com.discord.simpleast.core.node.Node
import com.discord.simpleast.core.parser.Parser
typealias MarkdownParser = Parser<MarkdownContext, Node<MarkdownContext>, MarkdownState>
data class MarkdownState(val currentQuoteDepth: Int) {
fun newQuoteDepth(depth: Int): MarkdownState = MarkdownState(depth)
}
data class MarkdownContext(
val memberMap: SnapshotStateMap<String, String>,
val userMap: SnapshotStateMap<String, User>,
val channelMap: SnapshotStateMap<String, String>,
val serverId: String,
val memberMap: Map<String, String>,
val userMap: Map<String, User>,
val channelMap: Map<String, String>,
val emojiMap: Map<String, Emoji>,
val serverId: String?
)

View File

@ -0,0 +1,32 @@
package chat.revolt.internals.markdown
import android.text.SpannableStringBuilder
import com.discord.simpleast.core.node.Node
class UserMentionNode(private val userId: String) : Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
builder.append(
renderContext.memberMap[userId]?.let { "@$it" }
?: renderContext.userMap[userId]?.let { "@${it.username}" }
?: "<@${userId}>"
)
}
}
class ChannelMentionNode(private val channelId: String) : Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
builder.append(
renderContext.channelMap[channelId]?.let { "#$it" }
?: "<#${channelId}>"
)
}
}
class CustomEmoteNode(private val emoteId: String) : Node<MarkdownContext>() {
override fun render(builder: SpannableStringBuilder, renderContext: MarkdownContext) {
builder.append(
renderContext.emojiMap[emoteId]?.let { ":${it.name}:" }
?: ":${emoteId}:"
)
}
}

View File

@ -0,0 +1,138 @@
package chat.revolt.internals.markdown
import android.content.Context
import android.text.style.BackgroundColorSpan
import android.text.style.TextAppearanceSpan
import chat.revolt.R
import com.discord.simpleast.code.CodeRules
import com.discord.simpleast.code.CodeStyleProviders
import com.discord.simpleast.core.node.Node
import com.discord.simpleast.core.node.StyleNode
import com.discord.simpleast.core.parser.ParseSpec
import com.discord.simpleast.core.parser.Parser
import com.discord.simpleast.core.parser.Rule
import java.util.regex.Matcher
import java.util.regex.Pattern
class UserMentionRule<S> :
Rule<MarkdownContext, UserMentionNode, S>(Pattern.compile("^<@([0-9A-Z]{26})>")) {
override fun parse(
matcher: Matcher,
parser: Parser<MarkdownContext, in UserMentionNode, S>,
state: S
): ParseSpec<MarkdownContext, S> {
return ParseSpec.createTerminal(UserMentionNode(matcher.group(1)!!), state)
}
}
class ChannelMentionRule<S> :
Rule<MarkdownContext, ChannelMentionNode, S>(Pattern.compile("^<#([0-9A-Z]{26})>")) {
override fun parse(
matcher: Matcher,
parser: Parser<MarkdownContext, in ChannelMentionNode, S>,
state: S
): ParseSpec<MarkdownContext, S> {
return ParseSpec.createTerminal(ChannelMentionNode(matcher.group(1)!!), state)
}
}
class CustomEmoteRule<S> :
Rule<MarkdownContext, CustomEmoteNode, S>(Pattern.compile("^:([0-9A-Z]{26}):")) {
override fun parse(
matcher: Matcher,
parser: Parser<MarkdownContext, in CustomEmoteNode, S>,
state: S
): ParseSpec<MarkdownContext, S> {
return ParseSpec.createTerminal(CustomEmoteNode(matcher.group(1)!!), state)
}
}
fun <RC, S> createInlineCodeRule(context: Context, backgroundColor: Int): Rule<RC, Node<RC>, S> {
return CodeRules.createInlineCodeRule(
{ listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
{ listOf(BackgroundColorSpan(backgroundColor)) }
)
}
fun <RC, S : MarkdownState> createCodeRule(
context: Context,
backgroundColor: Int
): Rule<RC, Node<RC>, S> {
val codeStyleProviders = CodeStyleProviders<RC>(
defaultStyleProvider = { listOf(TextAppearanceSpan(context, R.style.Code_TextAppearance)) },
commentStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Comment
)
)
},
literalStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Literal
)
)
},
keywordStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Keyword
)
)
},
identifierStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Identifier
)
)
},
typesStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Types
)
)
},
genericsStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Generics
)
)
},
paramsStyleProvider = {
listOf(
TextAppearanceSpan(
context,
R.style.Code_TextAppearance_Params
)
)
},
)
val languageMap = CodeRules.createCodeLanguageMap<RC, S>(codeStyleProviders)
return CodeRules.createCodeRule(
codeStyleProviders.defaultStyleProvider,
languageMap
) { codeNode, block, state ->
if (!block) {
StyleNode<RC, Any>(listOf(BackgroundColorSpan(backgroundColor)))
.apply { addChild(codeNode) }
} else {
BlockBackgroundNode(
state.currentQuoteDepth,
backgroundColor,
backgroundColor,
codeNode
)
}
}
}

View File

@ -28,7 +28,6 @@ import chat.revolt.api.routes.user.blockUser
import chat.revolt.api.schemas.ContentReportReason
import chat.revolt.components.chat.Message
import chat.revolt.components.generic.FormTextField
import chat.revolt.markdown.Markdown
import kotlinx.coroutines.launch
enum class ReportingState {
@ -112,7 +111,7 @@ fun ReportMessageDialog(
if (messageIsBridged) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = Markdown.annotate(stringResource(id = R.string.report_message_bridge_notice)),
text = stringResource(id = R.string.report_message_bridge_notice),
fontSize = 12.sp
)
}

View File

@ -19,6 +19,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@ -55,6 +56,16 @@ import chat.revolt.components.screens.chat.AttachmentManager
import chat.revolt.components.screens.chat.ChannelIcon
import chat.revolt.components.screens.chat.ReplyManager
import chat.revolt.components.screens.chat.TypingIndicator
import chat.revolt.internals.markdown.ChannelMentionRule
import chat.revolt.internals.markdown.CustomEmoteRule
import chat.revolt.internals.markdown.MarkdownContext
import chat.revolt.internals.markdown.MarkdownParser
import chat.revolt.internals.markdown.MarkdownState
import chat.revolt.internals.markdown.UserMentionRule
import chat.revolt.internals.markdown.createCodeRule
import chat.revolt.internals.markdown.createInlineCodeRule
import com.discord.simpleast.core.simple.SimpleMarkdownRules
import com.discord.simpleast.core.simple.SimpleRenderer
import io.ktor.http.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -401,6 +412,8 @@ fun ChannelScreen(
val lazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val codeBlockColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
val pickFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenMultipleDocuments()
) { uriList ->
@ -520,7 +533,39 @@ fun ChannelScreen(
items = viewModel.renderableMessages,
key = { it.id!! }
) { message ->
Message(message) {
Message(message, parse = {
val parser = MarkdownParser()
.addRules(
SimpleMarkdownRules.createEscapeRule(),
UserMentionRule(),
ChannelMentionRule(),
CustomEmoteRule(),
)
.addRules(
createCodeRule(context, codeBlockColor.toArgb()),
createInlineCodeRule(context, codeBlockColor.toArgb()),
)
.addRules(
SimpleMarkdownRules.createSimpleMarkdownRules(
includeEscapeRule = false
)
)
SimpleRenderer.render(
source = it.content ?: "",
parser = parser,
initialState = MarkdownState(0),
renderContext = MarkdownContext(
memberMap = mapOf(),
userMap = RevoltAPI.userCache.toMap(),
channelMap = RevoltAPI.channelCache.mapValues { ch ->
ch.value.name ?: ch.value.id!!
},
emojiMap = RevoltAPI.emojiCache,
serverId = channel.server ?: "",
)
)
}) {
navController.navigate("message/${message.id}/menu")
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto">
<font
app:font="@font/inter_thin"
app:fontStyle="normal"
app:fontWeight="100" />
<font
app:font="@font/inter_extralight"
app:fontStyle="normal"
app:fontWeight="200" />
<font
app:font="@font/inter_light"
app:fontStyle="normal"
app:fontWeight="300" />
<font
app:font="@font/inter_regular"
app:fontStyle="normal"
app:fontWeight="400" />
<font
app:font="@font/inter_medium"
app:fontStyle="normal"
app:fontWeight="500" />
<font
app:font="@font/inter_semibold"
app:fontStyle="normal"
app:fontWeight="600" />
<font
app:font="@font/inter_bold"
app:fontStyle="normal"
app:fontWeight="700" />
<font
app:font="@font/inter_extrabold"
app:fontStyle="normal"
app:fontWeight="800" />
<font
app:font="@font/inter_black"
app:fontStyle="normal"
app:fontWeight="900" />
</font-family>

Binary file not shown.

View File

@ -155,7 +155,7 @@
<string name="report_message">Thank you for taking the time to report this message. Please provide a reason for reporting this message.</string>
<string name="report_message_preview">Selected message:</string>
<string name="report_message_bridge_notice">**Note:** This message may have been sent from another platform. It is recommended to also report the message on the platform it was sent from.</string>
<string name="report_message_bridge_notice">Note: This message may have been sent from another platform. It is recommended to also report the message on the platform it was sent from.</string>
<string name="report_server">Thank you for taking the time to report this server. Please provide a reason for reporting this server.</string>
<string name="report_server_preview">Selected server:</string>
<string name="report_user">Thank you for taking the time to report this user. Please provide a reason for reporting this user.</string>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Code.TextAppearance" parent="">
<item name="android:textSize">14sp</item>
<item name="android:textColor">#FFFFFF</item>
<item name="android:fontFamily">@font/jetbrainsmono_regular</item>
</style>
<style name="Code.TextAppearance.Comment" parent="">
<item name="android:textColor">#607d8b</item>
</style>
<style name="Code.TextAppearance.Literal" parent="">
<item name="android:textColor">#e91e63</item>
</style>
<style name="Code.TextAppearance.Keyword" parent="">
<item name="android:textColor">#2196f3</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Code.TextAppearance.Identifier" parent="">
<item name="android:textColor">#cddc39</item>
</style>
<style name="Code.TextAppearance.Types" parent="">
<item name="android:textColor">#00e676</item>
</style>
<style name="Code.TextAppearance.Generics" parent="">
<item name="android:textColor">#ff5722</item>
</style>
<style name="Code.TextAppearance.Params" parent="">
<item name="android:textColor">#009688</item>
</style>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Revolt" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.Revolt" parent="Theme.AppCompat.DayNight.NoActionBar" />
</resources>

1
markdown/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -1,50 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'chat.revolt.markdown'
compileSdk 33
defaultConfig {
minSdk 23
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation platform("androidx.compose:compose-bom:$compose_bom_version")
implementation "androidx.compose.ui:ui"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -1,28 +0,0 @@
package chat.revolt.markdown
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
object Markdown {
fun <Ctx> annotateInContext(text: String, context: Ctx): AnnotatedString {
// TODO this is all placeholder code
val boldRegex = Regex("\\*\\*(.*?)\\*\\*")
return buildAnnotatedString {
append(text)
boldRegex.findAll(text).forEach { match ->
addStyle(
style = SpanStyle(fontWeight = FontWeight.Bold),
start = match.groups[1]!!.range.first,
end = match.groups[1]!!.range.last + 1
)
}
toAnnotatedString()
}
}
fun annotate(text: String): AnnotatedString = annotateInContext(text, Unit)
}

View File

@ -1,91 +0,0 @@
package chat.revolt.markdown.ast
/*
* SPECS:
* - Unist, the universal syntax tree @ https://github.com/syntax-tree/unist
* - Mdast, the markdown syntax tree @ https://github.com/syntax-tree/mdast
*/
open class Literal(
/**
* The value of a node.
*/
open val value: String = ""
) : Node()
open class Parent(
/**
* List representing the children of a node.
*/
val children: List<Node> = emptyList()
) : Node()
open class Root(
override val type: String = "root"
) : Parent()
open class Paragraph(
override val type: String = "paragraph"
) : Parent()
open class Text(
override val type: String = "text",
override val value: String = ""
) : Literal()
open class Heading(
override val type: String = "heading",
val depth: Int = 1
) : Parent()
open class ThematicBreak(
override val type: String = "thematicBreak"
) : Node()
open class Blockquote(
override val type: String = "blockquote"
) : Parent()
open class MdList(
override val type: String = "list",
val ordered: Boolean = false,
val start: Int = 1,
val spread: Boolean = false
) : Parent()
open class ListItem(
override val type: String = "listItem",
val spread: Boolean = false
) : Parent()
open class Code(
override val type: String = "code",
val lang: String? = null,
val meta: String? = null
) : Literal()
open class Emphasis(
override val type: String = "emphasis"
) : Parent()
open class Strong(
override val type: String = "strong"
) : Parent()
open class Delete(
override val type: String = "delete"
) : Parent()
open class InlineCode(
override val type: String = "inlineCode"
) : Literal()
open class Break(
override val type: String = "break"
) : Node()
open class Link(
override val type: String = "link",
val title: String? = null,
val url: String = ""
) : Parent()

View File

@ -1,60 +0,0 @@
package chat.revolt.markdown.ast
/*
* SPECS:
* - Unist, the universal syntax tree @ https://github.com/syntax-tree/unist
* - Mdast, the markdown syntax tree @ https://github.com/syntax-tree/mdast
*/
class Point(
/**
* Line number in the document, starting at 1.
*/
val line: Int = 1,
/**
* Column on line in the document, starting at 1.
*/
val column: Int = 1,
/**
* Character offset in the document, starting at 0.
*/
val offset: Int = 0
)
class Position(
/**
* Place of the first character of the parsed source region.
*/
val start: Point = Point(),
/**
* Place of the first character after the parsed source region.
*/
val end: Point = Point(),
/**
* Start column at each index (plus start line) in the source region,
* for elements that span multiple lines.
*/
val indent: List<Int> = emptyList()
)
open class Node(
/**
* The variant of the node.
*/
open val type: String = "",
/**
* Information from the ecosystem.
*/
val data: Map<String, Any> = emptyMap(),
/**
* Location of the node in a source document.
* Must not be present if a node is generated.
*/
val position: Position = Position()
)

View File

@ -1,4 +0,0 @@
package chat.revolt.markdown.parser
class Parser {
}

View File

@ -1,17 +0,0 @@
package chat.revolt.markdown
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -10,8 +10,8 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}
rootProject.name = "Revolt"
include ':app'
include ':markdown'