Skip to content

Commit

Permalink
Merge pull request #345 from MohamedRejeb/1.x
Browse files Browse the repository at this point in the history
Support loading images from markdown
  • Loading branch information
MohamedRejeb authored Sep 21, 2024
2 parents 1ccde4d + 4f79de7 commit 04bba29
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,11 @@ import com.mohamedrejeb.richeditor.model.ImageLoader
public object Coil3ImageLoader: ImageLoader {

@Composable
override fun load(model: Any): ImageData {
override fun load(model: Any): ImageData? {
val painter = rememberAsyncImagePainter(model = model)

var imageData by remember {
mutableStateOf<ImageData>(
ImageData(
painter = painter
)
)
mutableStateOf<ImageData?>(null)
}

LaunchedEffect(painter.state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public val LocalImageLoader: ProvidableCompositionLocal<ImageLoader> = staticCom
public class ImageData(
public val painter: Painter,
public val contentDescription: String? = null,
public val alignment: Alignment = Alignment.CenterStart,
public val alignment: Alignment = Alignment.Center,
public val contentScale: ContentScale = ContentScale.Fit,
public val modifier: Modifier = Modifier.fillMaxWidth()
)
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public interface RichSpanStyle {
private val cornerRadius: TextUnit = 8.sp,
private val strokeWidth: TextUnit = 1.sp,
private val padding: TextPaddingValues = TextPaddingValues(horizontal = 2.sp, vertical = 2.sp)
): RichSpanStyle {
) : RichSpanStyle {
override val spanStyle: (RichTextConfig) -> SpanStyle = {
SpanStyle(
color = it.codeSpanColor,
Expand Down Expand Up @@ -172,8 +172,23 @@ public interface RichSpanStyle {
public val model: Any,
width: TextUnit,
height: TextUnit,
public val contentDescription: String? = null,
) : RichSpanStyle {

init {
require(width.isSpecified || height.isSpecified) {
"At least one of the width or height should be specified"
}

require(width.value >= 0 || height.value >= 0) {
"The width and height should be greater than or equal to 0"
}

require(width.value.isFinite() || height.value.isFinite()) {
"The width and height should be finite"
}
}

public var width: TextUnit by mutableStateOf(width)
private set

Expand Down Expand Up @@ -202,9 +217,7 @@ public interface RichSpanStyle {

richTextState.usedInlineContentMapKeys.add(id)

appendInlineContent(
id = id,
)
appendInlineContent(id = id)

return this
}
Expand All @@ -219,48 +232,44 @@ public interface RichSpanStyle {
placeholderVerticalAlign = PlaceholderVerticalAlign.TextBottom
),
children = {
key(id) {
val density = LocalDensity.current
val imageLoader = LocalImageLoader.current
val data = imageLoader.load(model) ?: return@InlineTextContent
val density = LocalDensity.current
val imageLoader = LocalImageLoader.current
val data = imageLoader.load(model) ?: return@InlineTextContent

LaunchedEffect(id, data) {
if (data.painter.intrinsicSize.isUnspecified)
return@LaunchedEffect
LaunchedEffect(id, data) {
if (data.painter.intrinsicSize.isUnspecified)
return@LaunchedEffect

val newWidth = with(density) {
data.painter.intrinsicSize.width.coerceAtLeast(0f).toSp()
}
val newHeight = with(density) {
data.painter.intrinsicSize.height.coerceAtLeast(0f).toSp()
}
val newWidth = with(density) {
data.painter.intrinsicSize.width.coerceAtLeast(0f).toSp()
}
val newHeight = with(density) {
data.painter.intrinsicSize.height.coerceAtLeast(0f).toSp()
}

if (width == newWidth && height == newHeight)
return@LaunchedEffect
if (width == newWidth && height == newHeight)
return@LaunchedEffect

richTextState.inlineContentMap.remove(id)
richTextState.usedInlineContentMapKeys.remove(id)
richTextState.inlineContentMap.remove(id)

if (width.isUnspecified || width.value <= 0)
width = newWidth
if (width.isUnspecified || width.value <= 0)
width = newWidth

if (height.isUnspecified || height.value <= 0)
height = newHeight
if (height.isUnspecified || height.value <= 0)
height = newHeight

richTextState.inlineContentMap[id] = createInlineTextContent(richTextState = richTextState)
richTextState.usedInlineContentMapKeys.add(id)
richTextState.updateAnnotatedString()
}

Image(
painter = data.painter,
contentDescription = data.contentDescription,
alignment = data.alignment,
contentScale = data.contentScale,
modifier = data.modifier
.fillMaxSize()
)
richTextState.inlineContentMap[id] = createInlineTextContent(richTextState = richTextState)
richTextState.updateAnnotatedString()
}

Image(
painter = data.painter,
contentDescription = data.contentDescription ?: contentDescription,
alignment = data.alignment,
contentScale = data.contentScale,
modifier = data.modifier
.fillMaxSize()
)
}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ internal object RichTextStateHtmlParser : RichTextStateParser<String> {
model = attributes["src"].orEmpty(),
width = (attributes["width"]?.toIntOrNull() ?: 0).sp,
height = (attributes["height"]?.toIntOrNull() ?: 0).sp,
contentDescription = attributes["alt"] ?: ""
)

else ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.mohamedrejeb.richeditor.parser.markdown
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
Expand Down Expand Up @@ -79,6 +80,25 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
currentRichSpan = safeCurrentRichSpan
currentRichParagraph.children.add(safeCurrentRichSpan)
}

val currentRichSpanRichSpanStyle = currentRichSpan?.richSpanStyle
val lastOpenedNode = openedNodes.lastOrNull()

if (lastOpenedNode?.type == MarkdownElementTypes.IMAGE && text == "!") {
currentRichSpan?.text = ""
}

if (currentRichSpanRichSpanStyle is RichSpanStyle.Image) {
currentRichSpan?.richSpanStyle =
RichSpanStyle.Image(
model = currentRichSpanRichSpanStyle.model,
width = currentRichSpanRichSpanStyle.width,
height = currentRichSpanRichSpanStyle.height,
contentDescription = text
)

currentRichSpan?.text = ""
}
}

encodeMarkdownToRichText(
Expand Down Expand Up @@ -397,21 +417,36 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
node: ASTNode,
markdown: String,
): RichSpanStyle {
val isImage = node.parent?.type == MarkdownElementTypes.IMAGE

return when (node.type) {
GFMTokenTypes.GFM_AUTOLINK -> {
val destination = node.getTextInNode(markdown).toString()
RichSpanStyle.Link(url = destination)
}

MarkdownElementTypes.INLINE_LINK -> {
val destination = node
.findChildOfType(MarkdownElementTypes.LINK_DESTINATION)
?.getTextInNode(markdown)
?.toString()
.orEmpty()
RichSpanStyle.Link(url = destination)

if (isImage)
RichSpanStyle.Image(
model = destination,
width = 0.sp,
height = 0.sp,
)
else
RichSpanStyle.Link(url = destination)
}
MarkdownElementTypes.CODE_SPAN -> RichSpanStyle.Code()
else -> RichSpanStyle.Default

MarkdownElementTypes.CODE_SPAN ->
RichSpanStyle.Code()

else ->
RichSpanStyle.Default
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,28 @@ class RichTextStateMarkdownParserEncodeTest {
assertEquals(linkRichSpanStyle.url, "https://www.google.com")
}

@OptIn(ExperimentalRichTextApi::class)
@Test
fun testEncodeMarkdownWithImage() {
val imageUrl = "https://www.imageurl.com"
val imageAlt = "image-alt"

val markdown = "Image: ![$imageAlt]($imageUrl)"
val expectedText = "Image: "
val state = RichTextStateMarkdownParser.encode(markdown)
val actualText = state.annotatedString.text

assertEquals(
expected = expectedText,
actual = actualText.dropLast(1),
)

val imageRichSpan = state.richParagraphList.first().children[1]
val imageRichSpanStyle = imageRichSpan.richSpanStyle

assertIs<RichSpanStyle.Image>(imageRichSpanStyle)
assertEquals(imageUrl, imageRichSpanStyle.model)
assertEquals(imageAlt, imageRichSpanStyle.contentDescription)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.coil3.Coil3ImageLoader
import com.mohamedrejeb.richeditor.model.rememberRichTextState
import com.mohamedrejeb.richeditor.ui.material3.RichText

@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalRichTextApi::class)
@Composable
fun MarkdownToRichText(
markdown: TextFieldValue,
Expand Down Expand Up @@ -84,6 +86,7 @@ fun MarkdownToRichText(
item {
RichText(
state = richTextState,
imageLoader = Coil3ImageLoader,
modifier = Modifier
.fillMaxWidth()
)
Expand Down

0 comments on commit 04bba29

Please sign in to comment.