Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Support loading images from markdown #345

Merged
merged 1 commit into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading