Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.commonmark:commonmark:0.27.0"
}
135 changes: 129 additions & 6 deletions android/src/main/java/com/richtext/RichTextView.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,138 @@
package com.richtext

import android.content.Context
import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import com.facebook.react.common.ReactConstants
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
import com.richtext.parser.Parser
import com.richtext.renderer.Renderer

class RichTextView : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
class RichTextView : AppCompatTextView {

private val parser = Parser()
private val renderer = Renderer()
private var onLinkPressCallback: ((String) -> Unit)? = null

private var typefaceDirty = false
private var didAttachToWindow = false

var fontSize: Float? = null
private var fontFamily: String? = null
private var fontStyle: Int = ReactConstants.UNSET
private var fontWeight: Int = ReactConstants.UNSET

constructor(context: Context) : super(context) {
prepareComponent()
}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
prepareComponent()
}

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
) {
prepareComponent()
}

private fun prepareComponent() {
movementMethod = LinkMovementMethod.getInstance()
setPadding(0, 0, 0, 0)
setBackgroundColor(Color.TRANSPARENT)
}

fun setMarkdownContent(markdown: String) {
try {
val document = parser.parseMarkdown(markdown)
if (document != null) {
val styledText = renderer.renderDocument(document, onLinkPressCallback)
text = styledText
movementMethod = LinkMovementMethod.getInstance()
} else {
android.util.Log.e("RichTextView", "Failed to parse markdown - Document is null")
text = ""
}
} catch (e: Exception) {
android.util.Log.e("RichTextView", "Error parsing markdown: ${e.message}")
text = ""
}
}


fun setOnLinkPressCallback(callback: (String) -> Unit) {
onLinkPressCallback = callback
}

fun emitOnLinkPress(url: String) {
val context = this.context as? com.facebook.react.bridge.ReactContext ?: return
val surfaceId = com.facebook.react.uimanager.UIManagerHelper.getSurfaceId(context)
val dispatcher =
com.facebook.react.uimanager.UIManagerHelper.getEventDispatcherForReactTag(context, id)

dispatcher?.dispatchEvent(
com.richtext.events.LinkPressEvent(
surfaceId,
id,
url
)
)
}

fun setFontSize(size: Float) {
fontSize = size
textSize = size
typefaceDirty = true
updateTypeface()
}

fun setFontFamily(family: String?) {
fontFamily = family
typefaceDirty = true
updateTypeface()
}

fun setFontWeight(weight: String?) {
val parsedWeight = parseFontWeight(weight)
if (parsedWeight != fontWeight) {
fontWeight = parsedWeight
typefaceDirty = true
updateTypeface()
}
}

fun setFontStyle(style: String?) {
val parsedStyle = parseFontStyle(style)
if (parsedStyle != fontStyle) {
fontStyle = parsedStyle
typefaceDirty = true
updateTypeface()
}
}

fun setColor(color: Int?) {
if (color != null) {
setTextColor(color)
}
}

fun updateTypeface() {
if (!typefaceDirty) return
typefaceDirty = false

val newTypeface = applyStyles(typeface, fontStyle, fontWeight, fontFamily, context.assets)
setTypeface(newTypeface)
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
didAttachToWindow = true
updateTypeface()
}
}
74 changes: 62 additions & 12 deletions android/src/main/java/com/richtext/RichTextViewManager.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package com.richtext

import android.graphics.Color
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RichTextViewManagerInterface
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewProps
import com.facebook.react.uimanager.ViewDefaults
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.viewmanagers.RichTextViewManagerDelegate
import com.facebook.react.viewmanagers.RichTextViewManagerInterface
import com.richtext.events.LinkPressEvent

@ReactModule(name = RichTextViewManager.NAME)
class RichTextViewManager : SimpleViewManager<RichTextView>(),
RichTextViewManagerInterface<RichTextView> {
private val mDelegate: ViewManagerDelegate<RichTextView>

init {
mDelegate = RichTextViewManagerDelegate(this)
}
private val mDelegate: ViewManagerDelegate<RichTextView> = RichTextViewManagerDelegate(this)

override fun getDelegate(): ViewManagerDelegate<RichTextView>? {
return mDelegate
Expand All @@ -26,13 +26,63 @@ class RichTextViewManager : SimpleViewManager<RichTextView>(),
return NAME
}

public override fun createViewInstance(context: ThemedReactContext): RichTextView {
return RichTextView(context)
override fun createViewInstance(reactContext: ThemedReactContext): RichTextView {
return RichTextView(reactContext)
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
val map = mutableMapOf<String, Any>()
map.put(LinkPressEvent.EVENT_NAME, mapOf("registrationName" to LinkPressEvent.EVENT_NAME))
return map
}

@ReactProp(name = "markdown")
override fun setMarkdown(view: RichTextView?, markdown: String?) {
view?.setOnLinkPressCallback { url ->
emitOnLinkPress(view, url)
}

view?.setMarkdownContent(markdown ?: "No markdown content")
}


@ReactProp(name = "fontSize", defaultInt = ViewDefaults.FONT_SIZE_SP.toInt())
override fun setFontSize(view: RichTextView?, fontSize: Int) {
view?.setFontSize(fontSize.toFloat())
}

@ReactProp(name = "fontFamily")
override fun setFontFamily(view: RichTextView?, family: String?) {
view?.setFontFamily(family)
}

@ReactProp(name = ViewProps.COLOR, customType = "Color")
override fun setColor(view: RichTextView?, color: Int?) {
view?.setColor(color)
}

@ReactProp(name = "fontWeight")
override fun setFontWeight(view: RichTextView?, weight: String?) {
view?.setFontWeight(weight)
}

@ReactProp(name = "fontStyle")
override fun setFontStyle(view: RichTextView?, style: String?) {
view?.setFontStyle(style)
}

@ReactProp(name = "color")
override fun setColor(view: RichTextView?, color: String?) {
view?.setBackgroundColor(Color.parseColor(color))
override fun onAfterUpdateTransaction(view: RichTextView) {
super.onAfterUpdateTransaction(view)
view.updateTypeface()
}

private fun emitOnLinkPress(view: RichTextView, url: String) {
val context = view.context as com.facebook.react.bridge.ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
val event = LinkPressEvent(surfaceId, view.id, url)

eventDispatcher?.dispatchEvent(event)
}

companion object {
Expand Down
23 changes: 23 additions & 0 deletions android/src/main/java/com/richtext/events/LinkPressEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.richtext.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class LinkPressEvent(surfaceId: Int, viewId: Int, private val url: String) :
Event<LinkPressEvent>(surfaceId, viewId) {

override fun getEventName(): String {
return EVENT_NAME
}

override fun getEventData(): WritableMap {
val eventData: WritableMap = Arguments.createMap()
eventData.putString("url", url)
return eventData
}

companion object {
const val EVENT_NAME: String = "onLinkPress"
}
}
30 changes: 30 additions & 0 deletions android/src/main/java/com/richtext/parser/Parser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.richtext.parser

import android.util.Log
import org.commonmark.node.Document
import org.commonmark.parser.Parser

class Parser {

private val parser = Parser.builder().build()

fun parseMarkdown(markdown: String): Document? {
if (markdown.isBlank()) {
return null
}

try {
val document = parser.parse(markdown) as? Document

if (document != null) {
return document
} else {
Log.w("MarkdownParser", "Failed to cast parsed result to Document")
return null
}
} catch (e: Exception) {
Log.e("MarkdownParser", "CommonMark parsing failed: ${e.message}")
return null
}
}
}
Loading