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

Trim paragraphs on converting to and from Markdown #411

Merged
merged 1 commit into from
Nov 10, 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 @@ -9,6 +9,7 @@ import com.mohamedrejeb.richeditor.annotation.ExperimentalRichTextApi
import com.mohamedrejeb.richeditor.paragraph.RichParagraph
import com.mohamedrejeb.richeditor.utils.customMerge
import com.mohamedrejeb.richeditor.utils.isSpecifiedFieldsEquals
import kotlin.collections.indices

/**
* A rich span is a part of a rich paragraph.
Expand Down Expand Up @@ -232,6 +233,94 @@ internal class RichSpan(
return null
}

/**
* Trim the start of the rich span
*
* @return True if the rich span is empty after trimming, false otherwise
*/
internal fun trimStart(): Boolean {
if (richSpanStyle is RichSpanStyle.Image)
return false

if (isBlank()) {
text = ""
children.clear()
return true
}

text = text.trimStart()

if (text.isNotEmpty())
return false

var isEmpty = true
val toRemoveIndices = mutableListOf<Int>()

for (i in children.indices) {
val richSpan = children[i]

val isChildEmpty = richSpan.trimStart()

if (isChildEmpty) {
// Remove the child if it's empty
toRemoveIndices.add(i)
} else {
isEmpty = false
break
}
}

toRemoveIndices.fastForEachReversed {
children.removeAt(it)
}

return isEmpty
}

internal fun trimEnd(): Boolean {
val isImage = richSpanStyle is RichSpanStyle.Image

if (isImage)
return false

val isChildrenBlank = isChildrenBlank() && !isImage

if (text.isBlank() && isChildrenBlank) {
text = ""
children.clear()
return true
}

if (isChildrenBlank) {
children.clear()
text = text.trimEnd()
return false
}

var isEmpty = true
val toRemoveIndices = mutableListOf<Int>()

for (i in children.indices.reversed()) {
val richSpan = children[i]

val isChildEmpty = richSpan.trimEnd()

if (isChildEmpty) {
// Remove the child if it's empty
toRemoveIndices.add(i)
} else {
isEmpty = false
break
}
}

toRemoveIndices.fastForEach {
children.removeAt(it)
}

return isEmpty
}

/**
* Get the last non-empty child
*
Expand Down Expand Up @@ -497,6 +586,6 @@ internal class RichSpan(
)

override fun toString(): String {
return "richSpan(text='$text', textRange=$textRange, fullTextRange=$fullTextRange), richSpanStyle=$richSpanStyle)"
return "richSpan(text='$text', textRange=$textRange, fullTextRange=$fullTextRange, fontSize=${spanStyle.fontSize}, fontWeight=${spanStyle.fontWeight}, richSpanStyle=$richSpanStyle)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,47 @@ internal class RichParagraph(
return firstChild
}

/**
* Trim the rich paragraph
*/
fun trim() {
val isEmpty = trimStart()
if (!isEmpty)
trimEnd()
}

/**
* Trim the start of the rich paragraph
*
* @return True if the rich paragraph is empty after trimming, false otherwise
*/
fun trimStart(): Boolean {
children.fastForEach { richSpan ->
val isEmpty = richSpan.trimStart()

if (!isEmpty)
return false
}

return true
}

/**
* Trim the end of the rich paragraph
*
* @return True if the rich paragraph is empty after trimming, false otherwise
*/
fun trimEnd(): Boolean {
children.fastForEachReversed { richSpan ->
val isEmpty = richSpan.trimEnd()

if (!isEmpty)
return false
}

return true
}

/**
* Update the paragraph of the children recursively
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
var lastBrParagraphIndex = -1

richParagraphList.forEachIndexed { i, paragraph ->
paragraph.trim()

val isEmpty = paragraph.isEmpty()
val isBr = i in brParagraphIndices

Expand Down Expand Up @@ -341,16 +343,20 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
// Append paragraph start text
builder.appendParagraphStartText(richParagraph)

var isHeading = false

richParagraph.getFirstNonEmptyChild()?.let { firstNonEmptyChild ->
if (firstNonEmptyChild.text.isNotEmpty()) {
// Append markdown line start text
builder.append(getMarkdownLineStartTextFromFirstRichSpan(firstNonEmptyChild))
val lineStartText = getMarkdownLineStartTextFromFirstRichSpan(firstNonEmptyChild)
builder.append(lineStartText)
isHeading = lineStartText.startsWith('#')
}
}

// Append paragraph children
richParagraph.children.fastForEach { richSpan ->
builder.append(decodeRichSpanToMarkdown(richSpan))
builder.append(decodeRichSpanToMarkdown(richSpan, isHeading))
}

// Append line break if needed
Expand All @@ -371,7 +377,10 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
}

@OptIn(ExperimentalRichTextApi::class)
private fun decodeRichSpanToMarkdown(richSpan: RichSpan): String {
private fun decodeRichSpanToMarkdown(
richSpan: RichSpan,
isHeading: Boolean,
): String {
val stringBuilder = StringBuilder()

// Check if span is empty
Expand All @@ -384,7 +393,8 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
val markdownOpen = mutableListOf<String>()
val markdownClose = mutableListOf<String>()

if ((richSpan.spanStyle.fontWeight?.weight ?: 400) > 400) {
// Ignore adding bold `**` for heading since it's already bold
if ((richSpan.spanStyle.fontWeight?.weight ?: 400) > 400 && !isHeading) {
markdownOpen += "**"
markdownClose += "**"
}
Expand All @@ -405,7 +415,7 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {
}

// Append markdown open
if (!isBlank)
if (!isBlank && markdownOpen.isNotEmpty())
stringBuilder.append(markdownOpen.joinToString(separator = ""))

// Apply rich span style to markdown
Expand All @@ -416,11 +426,11 @@ internal object RichTextStateMarkdownParser : RichTextStateParser<String> {

// Append children
richSpan.children.fastForEach { child ->
stringBuilder.append(decodeRichSpanToMarkdown(child))
stringBuilder.append(decodeRichSpanToMarkdown(child, isHeading))
}

// Append markdown close
if (!isBlank)
if (!isBlank && markdownClose.isNotEmpty())
stringBuilder.append(markdownClose.reversed().joinToString(separator = ""))

return stringBuilder.toString()
Expand Down

This file was deleted.

Loading
Loading