From 5b8f26e7145a90af1aae523e81ead2127220afed Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 20 Dec 2024 19:13:09 +0100 Subject: [PATCH] added support for nested lists --- .../richeditor/model/RichTextState.kt | 154 +++++++++++++++--- .../paragraph/type/DefaultParagraph.kt | 2 +- .../paragraph/type/ListNestedLevel.kt | 40 +++++ .../richeditor/paragraph/type/OrderedList.kt | 21 ++- .../paragraph/type/ParagraphType.kt | 2 +- .../paragraph/type/UnorderedList.kt | 15 +- .../parser/html/RichTextStateHtmlParser.kt | 109 ++++++++++--- .../markdown/RichTextStateMarkdownParser.kt | 16 +- 8 files changed, 299 insertions(+), 60 deletions(-) create mode 100644 richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ListNestedLevel.kt diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt index ad6305e2..79f8a1f5 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt @@ -91,6 +91,9 @@ public class RichTextState internal constructor( ?: RichSpanStyle.Default ) + public var currentListNestedLevel: ListNestedLevel by mutableStateOf(ListNestedLevel.LEVEL_1) + private set + /** * Returns whether the current selected text is a link. */ @@ -196,6 +199,7 @@ public class RichTextState internal constructor( public val isUnorderedList: Boolean get() = currentRichParagraphType is UnorderedList public val isOrderedList: Boolean get() = currentRichParagraphType is OrderedList + public val isList: Boolean get() = isUnorderedList || isOrderedList public val config: RichTextConfig = RichTextConfig( updateText = { @@ -898,6 +902,7 @@ public class RichTextState internal constructor( public fun toggleUnorderedList() { val paragraphs = getRichParagraphListByTextRange(selection) if (paragraphs.isEmpty()) return + currentListNestedLevel = ListNestedLevel.LEVEL_1 val removeUnorderedList = paragraphs.first().type is UnorderedList paragraphs.forEach { paragraph -> if (removeUnorderedList) { @@ -927,6 +932,7 @@ public class RichTextState internal constructor( public fun toggleOrderedList() { val paragraphs = getRichParagraphListByTextRange(selection) if (paragraphs.isEmpty()) return + currentListNestedLevel = ListNestedLevel.LEVEL_1 val removeOrderedList = paragraphs.first().type is OrderedList paragraphs.forEach { paragraph -> if (removeOrderedList) { @@ -953,6 +959,30 @@ public class RichTextState internal constructor( } } + public fun increaseListNestedLevel() { + if (!isList) return + currentListNestedLevel = currentListNestedLevel.getNextOrMax() + updateList() + } + + public fun decreaseListNestedLevel() { + if (!isList) return + currentListNestedLevel = currentListNestedLevel.getPreviousOrMin() + updateList() + } + + private fun updateList() { + val paragraphs = getRichParagraphListByTextRange(selection) + if (paragraphs.isEmpty()) return + paragraphs.forEach { paragraph -> + if (paragraph.type is OrderedList) { + updateOrderedList(paragraph) + } else if (paragraph.type is UnorderedList) { + updateUnorderedList(paragraph) + } + } + } + /** * Private/Internal methods */ @@ -989,12 +1019,13 @@ public class RichTextState internal constructor( if (paragraph.type is UnorderedList) return val newType = UnorderedList( - initialIndent = config.unorderedListIndent + initialIndent = config.unorderedListIndent, + initialNestedLevel = currentListNestedLevel ) updateParagraphType( paragraph = paragraph, - newType = newType + newType = newType, ) } @@ -1004,15 +1035,27 @@ public class RichTextState internal constructor( resetParagraphType(paragraph = paragraph) } + + private fun updateUnorderedList(paragraph: RichParagraph) { + if (paragraph.type !is UnorderedList) return + + val newType = UnorderedList( + initialIndent = config.unorderedListIndent, + initialNestedLevel = currentListNestedLevel + ) + + updateParagraphType( + paragraph = paragraph, + newType = newType, + ) + } + private fun addOrderedList(paragraph: RichParagraph) { if (paragraph.type is OrderedList) return val index = richParagraphList.indexOf(paragraph) if (index == -1) return - val previousParagraphType = richParagraphList.getOrNull(index - 1)?.type - val orderedListNumber = - if (previousParagraphType is OrderedList) - previousParagraphType.number + 1 - else 1 + + val orderedListNumber = getCurrentOrderedListNumber(index - 1, currentListNestedLevel) val newTextFieldValue = adjustOrderedListsNumbers( startParagraphIndex = index + 1, @@ -1050,6 +1093,78 @@ public class RichTextState internal constructor( resetParagraphType(paragraph = paragraph) } + + private fun updateOrderedList(paragraph: RichParagraph) { + if (paragraph.type !is OrderedList) return + val index = richParagraphList.indexOf(paragraph) + if (index == -1) return + + val orderedListNumber = getCurrentOrderedListNumber(index - 1, currentListNestedLevel) + + val newTextFieldValue = adjustOrderedListsNumbers( + startParagraphIndex = index + 1, + startNumber = orderedListNumber + 1, + textFieldValue = textFieldValue, + ) + + val firstRichSpan = paragraph.getFirstNonEmptyChild() + + val newType = OrderedList( + number = orderedListNumber, + initialIndent = config.orderedListIndent, + startTextSpanStyle = firstRichSpan?.spanStyle ?: SpanStyle(), + initialNestedLevel = currentListNestedLevel + ) + updateTextFieldValue( + newTextFieldValue = updateParagraphType( + paragraph = paragraph, + newType = newType, + textFieldValue = newTextFieldValue, + ), + ) + } + + private fun getCurrentOrderedListNumber(fromIndex: Int, newNestedLevel: ListNestedLevel): Int { + // the resulting list is in reverse order (e.g. last paragraph is first item) + val previousOrderedListParagraphs = mutableListOf() + + for (i in fromIndex downTo 0) { + val prevParagraph = richParagraphList[i] + if (prevParagraph.type is UnorderedList) continue + if (prevParagraph.type !is OrderedList) break + + val orderedListType = prevParagraph.type as OrderedList + previousOrderedListParagraphs.add(orderedListType) + + // Stop as soon as we encounter a LEVEL_1 OrderedList + if (orderedListType.nestedLevel == ListNestedLevel.LEVEL_1) break + } + + // If no preceding OrderedList paragraphs exist, start numbering from 1 + if (previousOrderedListParagraphs.isEmpty()) return 1 + + val lastParagraph = previousOrderedListParagraphs.first() + val lastLevel2Paragraph = previousOrderedListParagraphs.find { it.nestedLevel == ListNestedLevel.LEVEL_2 } + val lastLevel1Paragraph = previousOrderedListParagraphs.find { it.nestedLevel == ListNestedLevel.LEVEL_1 } + + return when { + // If the last paragraph has the same nesting level, increment its number + lastParagraph.nestedLevel == newNestedLevel -> lastParagraph.number + 1 + + // If the last paragraph has a lower nesting level, restart numbering + lastParagraph.nestedLevel.number < newNestedLevel.number -> 1 + + // If we're in LEVEL_2, continue numbering from the last LEVEL_2 paragraph + newNestedLevel == ListNestedLevel.LEVEL_2 && lastLevel2Paragraph != null -> lastLevel2Paragraph.number + 1 + + // If we're in LEVEL_1, continue numbering from the last LEVEL_1 paragraph + newNestedLevel == ListNestedLevel.LEVEL_1 && lastLevel1Paragraph != null -> lastLevel1Paragraph.number + 1 + + else -> 1 + } + } + + private fun updateParagraphType( paragraph: RichParagraph, newType: ParagraphType, @@ -1420,6 +1535,7 @@ public class RichTextState internal constructor( if (minRemoveIndex < minParagraphFirstChildMinIndex) { if (minRichSpan.paragraph.type.startText.isEmpty() && minParagraphIndex != maxParagraphIndex) { richParagraphList.removeAt(minParagraphIndex) + if (isList) currentListNestedLevel = ListNestedLevel.LEVEL_1 } else { handleRemoveMinParagraphStartText( removeIndex = minRemoveIndex, @@ -1661,7 +1777,8 @@ public class RichTextState internal constructor( number = number, initialIndent = config.orderedListIndent, startTextSpanStyle = currentParagraphType.startTextSpanStyle, - startTextWidth = currentParagraphType.startTextWidth + startTextWidth = currentParagraphType.startTextWidth, + initialNestedLevel = currentParagraphType.nestedLevel ), textFieldValue = newTextFieldValue, ) @@ -1675,32 +1792,27 @@ public class RichTextState internal constructor( startParagraphIndex: Int, endParagraphIndex: Int, ) { - var number = 1 - val startParagraph = richParagraphList.getOrNull(startParagraphIndex) - val startParagraphType = startParagraph?.type - if (startParagraphType is OrderedList) { - number = startParagraphType.number + 1 - } // Update the paragraph type of the paragraphs after the new paragraph for (i in (startParagraphIndex + 1)..richParagraphList.lastIndex) { val currentParagraph = richParagraphList[i] val currentParagraphType = currentParagraph.type if (currentParagraphType is OrderedList) { + val selectionNestedLevel = currentParagraphType.nestedLevel + val number = getCurrentOrderedListNumber(i - 1, selectionNestedLevel) + tempTextFieldValue = updateParagraphType( paragraph = currentParagraph, newType = OrderedList( number = number, initialIndent = config.orderedListIndent, startTextSpanStyle = currentParagraphType.startTextSpanStyle, - startTextWidth = currentParagraphType.startTextWidth + startTextWidth = currentParagraphType.startTextWidth, + initialNestedLevel = currentParagraphType.nestedLevel ), textFieldValue = tempTextFieldValue, ) - number++ } else if (i >= endParagraphIndex) break - else - number = 1 } } @@ -3165,12 +3277,11 @@ public class RichTextState internal constructor( } private fun checkParagraphsType() { - var orderedListNumber = 0 var orderedListStartTextSpanStyle = SpanStyle() - richParagraphList.forEach { richParagraph -> + richParagraphList.forEachIndexed { index, richParagraph -> val type = richParagraph.type if (type is OrderedList) { - orderedListNumber++ + val orderedListNumber = getCurrentOrderedListNumber(index - 1, type.nestedLevel) if (orderedListNumber == 1) orderedListStartTextSpanStyle = richParagraph.getFirstNonEmptyChild()?.spanStyle ?: SpanStyle() @@ -3178,7 +3289,6 @@ public class RichTextState internal constructor( type.number = orderedListNumber type.startTextSpanStyle = orderedListStartTextSpanStyle } else { - orderedListNumber = 0 orderedListStartTextSpanStyle = SpanStyle() } } diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/DefaultParagraph.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/DefaultParagraph.kt index 182cb842..48f9cd7e 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/DefaultParagraph.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/DefaultParagraph.kt @@ -18,7 +18,7 @@ internal class DefaultParagraph : ParagraphType { override val startRichSpan: RichSpan = RichSpan(paragraph = RichParagraph(type = this)) - override fun getNextParagraphType(): ParagraphType = + override fun getNextParagraphType(nestedLevel: ListNestedLevel?): ParagraphType = DefaultParagraph() override fun copy(): ParagraphType = diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ListNestedLevel.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ListNestedLevel.kt new file mode 100644 index 00000000..58ebf1fc --- /dev/null +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ListNestedLevel.kt @@ -0,0 +1,40 @@ +package com.mohamedrejeb.richeditor.paragraph.type + +public enum class ListNestedLevel( + internal val indentMultiplier: Float, + internal val number: Int // the Number Must be the same as Level +) { + LEVEL_1(indentMultiplier = 1f, number = 1), + LEVEL_2(indentMultiplier = 2f, number = 2), + LEVEL_3(indentMultiplier = 3f, number = 3); + + internal fun getNextOrMax(): ListNestedLevel { + return when (this) { + LEVEL_1 -> LEVEL_2 + LEVEL_2 -> LEVEL_3 + LEVEL_3 -> LEVEL_3 + } + } + + internal fun getPreviousOrMin(): ListNestedLevel { + return when (this) { + LEVEL_1 -> LEVEL_1 + LEVEL_2 -> LEVEL_1 + LEVEL_3 -> LEVEL_2 + } + } + + + internal companion object { + val maxNestedLevel = LEVEL_3 + + fun getByNumber(number: Int): ListNestedLevel? { + return when (number) { + 1 -> LEVEL_1 + 2 -> LEVEL_2 + 3 -> LEVEL_3 + else -> null + } + } + } +} \ No newline at end of file diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/OrderedList.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/OrderedList.kt index cbd996ef..fe29b995 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/OrderedList.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/OrderedList.kt @@ -16,7 +16,8 @@ internal class OrderedList( number: Int, initialIndent: Int = DefaultListIndent, startTextSpanStyle: SpanStyle = SpanStyle(), - startTextWidth: TextUnit = 0.sp + startTextWidth: TextUnit = 0.sp, + initialNestedLevel: ListNestedLevel = ListNestedLevel.LEVEL_1 ) : ParagraphType, ConfigurableStartTextWidth { var number = number @@ -39,6 +40,12 @@ internal class OrderedList( private var indent = initialIndent + var nestedLevel = initialNestedLevel + set(value) { + field = value + startRichSpan = getNewStartRichSpan(startRichSpan.textRange) + } + private var style: ParagraphStyle = getNewParagraphStyle() @@ -54,8 +61,8 @@ internal class OrderedList( private fun getNewParagraphStyle() = ParagraphStyle( textIndent = TextIndent( - firstLine = (indent - startTextWidth.value).sp, - restLine = indent.sp + firstLine = ((indent * nestedLevel.indentMultiplier) - startTextWidth.value).sp, + restLine = (indent * nestedLevel.indentMultiplier).sp ) ) @@ -77,12 +84,13 @@ internal class OrderedList( ) } - override fun getNextParagraphType(): ParagraphType = + override fun getNextParagraphType(nestedLevel: ListNestedLevel?): ParagraphType = OrderedList( number = number + 1, initialIndent = indent, startTextSpanStyle = startTextSpanStyle, - startTextWidth = startTextWidth + startTextWidth = startTextWidth, + initialNestedLevel = nestedLevel ?: this.nestedLevel ) override fun copy(): ParagraphType = @@ -90,7 +98,8 @@ internal class OrderedList( number = number, initialIndent = indent, startTextSpanStyle = startTextSpanStyle, - startTextWidth = startTextWidth + startTextWidth = startTextWidth, + initialNestedLevel = nestedLevel ) override fun equals(other: Any?): Boolean { diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt index 7e40deba..206cd956 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/ParagraphType.kt @@ -10,7 +10,7 @@ internal interface ParagraphType { val startRichSpan: RichSpan - fun getNextParagraphType(): ParagraphType + fun getNextParagraphType(nestedLevel: ListNestedLevel? = null): ParagraphType fun copy(): ParagraphType diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/UnorderedList.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/UnorderedList.kt index eebcbdeb..2678fd0e 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/UnorderedList.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/paragraph/type/UnorderedList.kt @@ -13,6 +13,7 @@ import com.mohamedrejeb.richeditor.paragraph.RichParagraph internal class UnorderedList( initialIndent: Int = DefaultListIndent, startTextWidth: TextUnit = 0.sp, + initialNestedLevel: ListNestedLevel = ListNestedLevel.LEVEL_1 ): ParagraphType, ConfigurableStartTextWidth { override var startTextWidth: TextUnit = startTextWidth @@ -23,6 +24,8 @@ internal class UnorderedList( private var indent = initialIndent + public var nestedLevel = initialNestedLevel + private var style: ParagraphStyle = getParagraphStyle() @@ -38,8 +41,8 @@ internal class UnorderedList( private fun getParagraphStyle() = ParagraphStyle( textIndent = TextIndent( - firstLine = indent.sp, - restLine = (indent + startTextWidth.value).sp + firstLine = (indent * nestedLevel.indentMultiplier).sp, + restLine = ((indent * nestedLevel.indentMultiplier) + startTextWidth.value).sp ) ) @@ -50,16 +53,18 @@ internal class UnorderedList( text = "• ", ) - override fun getNextParagraphType(): ParagraphType = + override fun getNextParagraphType(nestedLevel: ListNestedLevel?): ParagraphType = UnorderedList( initialIndent = indent, - startTextWidth = startTextWidth + startTextWidth = startTextWidth, + initialNestedLevel = nestedLevel ?: this.nestedLevel ) override fun copy(): ParagraphType = UnorderedList( initialIndent = indent, - startTextWidth = startTextWidth + startTextWidth = startTextWidth, + initialNestedLevel = nestedLevel ) override fun equals(other: Any?): Boolean { diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/RichTextStateHtmlParser.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/RichTextStateHtmlParser.kt index a497a635..be44e26b 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/RichTextStateHtmlParser.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/RichTextStateHtmlParser.kt @@ -18,6 +18,7 @@ import com.mohamedrejeb.richeditor.parser.utils.* import com.mohamedrejeb.richeditor.utils.customMerge import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed +import com.mohamedrejeb.richeditor.paragraph.type.ListNestedLevel internal object RichTextStateHtmlParser : RichTextStateParser { @@ -29,6 +30,7 @@ internal object RichTextStateHtmlParser : RichTextStateParser { val lineBreakParagraphIndexSet = mutableSetOf() val toKeepEmptyParagraphIndexSet = mutableSetOf() var currentRichSpan: RichSpan? = null + var currentListNestedLevel = 0 // don't use enum to allow starting at 0 val handler = KsoupHtmlHandler .Builder() @@ -71,11 +73,18 @@ internal object RichTextStateHtmlParser : RichTextStateParser { openedTags.add(name to attributes) - if (name == "ul" || name == "ol" || name in skippedHtmlElements) { + if (name in skippedHtmlElements) { + return@onOpenTag + } + + if (name == "ul" || name == "ol") { // Todo: Apply ul/ol styling if exists + currentListNestedLevel = minOf(ListNestedLevel.maxNestedLevel.number, currentListNestedLevel + 1) return@onOpenTag } + + if (name == "body") { stringBuilder.clear() richParagraphList.clear() @@ -102,13 +111,14 @@ internal object RichTextStateHtmlParser : RichTextStateParser { currentRichParagraph.type is DefaultParagraph && isCurrentRichParagraphBlank ) { - val paragraphType = encodeHtmlElementToRichParagraphType(lastOpenedTag) + val paragraphType = encodeHtmlElementToRichParagraphType(lastOpenedTag, ListNestedLevel.getByNumber(currentListNestedLevel)) currentRichParagraph.type = paragraphType val cssParagraphStyle = CssEncoder.parseCssStyleMapToParagraphStyle(cssStyleMap) currentRichParagraph.paragraphStyle = currentRichParagraph.paragraphStyle.merge(cssParagraphStyle) } + if (isCurrentTagBlockElement) { val newRichParagraph = if (isCurrentRichParagraphBlank) @@ -118,7 +128,7 @@ internal object RichTextStateHtmlParser : RichTextStateParser { var paragraphType: ParagraphType = DefaultParagraph() if (name == "li" && lastOpenedTag != null) { - paragraphType = encodeHtmlElementToRichParagraphType(lastOpenedTag) + paragraphType = encodeHtmlElementToRichParagraphType(lastOpenedTag, ListNestedLevel.getByNumber(currentListNestedLevel)) } val cssParagraphStyle = CssEncoder.parseCssStyleMapToParagraphStyle(cssStyleMap) @@ -213,7 +223,12 @@ internal object RichTextStateHtmlParser : RichTextStateParser { currentRichSpan = null } - if (name == "ul" || name == "ol" || name in skippedHtmlElements) + if (name == "ul" || name == "ol") { + currentListNestedLevel = maxOf(0, currentListNestedLevel - 1) + return@onCloseTag + } + + if (name in skippedHtmlElements) return@onCloseTag if (name != BrElement) @@ -250,27 +265,77 @@ internal object RichTextStateHtmlParser : RichTextStateParser { override fun decode(richTextState: RichTextState): String { val builder = StringBuilder() + val openedListTagNames = mutableListOf() var lastParagraphGroupTagName: String? = null var isLastParagraphEmpty = false + var currentListNestedLevel: ListNestedLevel? = null + richTextState.richParagraphList.fastForEachIndexed { index, richParagraph -> val isParagraphEmpty = richParagraph.isEmpty() val paragraphGroupTagName = decodeHtmlElementFromRichParagraphType(richParagraph.type) - // Close last paragraph group tag if needed - if ( - (lastParagraphGroupTagName == "ol" || lastParagraphGroupTagName == "ul") && - (lastParagraphGroupTagName != paragraphGroupTagName) - ) builder.append("") + val paragraphNestedLevel = (richParagraph.type as? OrderedList)?.nestedLevel + ?: (richParagraph.type as? UnorderedList)?.nestedLevel + - // Open new paragraph group tag if needed + fun swapOpenedListTagOnTypeChange(index: Int) { + if ( + (lastParagraphGroupTagName == "ol" && paragraphGroupTagName == "ul") || + (lastParagraphGroupTagName == "ul" && paragraphGroupTagName == "ol") + ) { + openedListTagNames[index] = paragraphGroupTagName + } + } + + // Close and Open group tag on same nested level if ( - (paragraphGroupTagName == "ol" || paragraphGroupTagName == "ul") && - lastParagraphGroupTagName != paragraphGroupTagName - ) + ((lastParagraphGroupTagName == "ol" && paragraphGroupTagName == "ul") || + (lastParagraphGroupTagName == "ul" && paragraphGroupTagName == "ol")) && + paragraphNestedLevel == currentListNestedLevel + ) { + builder.append("") builder.append("<$paragraphGroupTagName>") + swapOpenedListTagOnTypeChange(paragraphNestedLevel?.number?.minus(1) ?: 0) + } + + if ( + lastParagraphGroupTagName in listOf("ol", "ul") || + paragraphGroupTagName in listOf("ol", "ul") + ) { + if (currentListNestedLevel == null) { // first list paragraph + repeat(paragraphNestedLevel?.number ?: 0) { + openedListTagNames.add(paragraphGroupTagName) + builder.append("<${paragraphGroupTagName}>") + } + } else { + if (paragraphNestedLevel == null) { + repeat(currentListNestedLevel!!.number) { builder.append("") } + } else { + if (currentListNestedLevel!! < paragraphNestedLevel) { + repeat(paragraphNestedLevel.number - currentListNestedLevel!!.number) { + openedListTagNames.add(paragraphGroupTagName) + swapOpenedListTagOnTypeChange(paragraphNestedLevel.number - 1) + builder.append("<$paragraphGroupTagName>") + } + } else if (currentListNestedLevel!! > paragraphNestedLevel) { + repeat(currentListNestedLevel!!.number - paragraphNestedLevel.number) { + swapOpenedListTagOnTypeChange(paragraphNestedLevel.number - 1) + builder.append("") + } + } + } + } + } + + currentListNestedLevel = paragraphNestedLevel + // Add line break if the paragraph is empty - else if (isParagraphEmpty) { + if ( + !((paragraphGroupTagName == "ol" || paragraphGroupTagName == "ul") && + lastParagraphGroupTagName != paragraphGroupTagName) && + isParagraphEmpty + ) { val skipAddingBr = isLastParagraphEmpty && richParagraph.isEmpty() && index == richTextState.richParagraphList.lastIndex @@ -308,15 +373,14 @@ internal object RichTextStateHtmlParser : RichTextStateParser { // Save last paragraph group tag name lastParagraphGroupTagName = paragraphGroupTagName - // Close last paragraph group tag if needed - if ( - (lastParagraphGroupTagName == "ol" || lastParagraphGroupTagName == "ul") && - index == richTextState.richParagraphList.lastIndex - ) builder.append("") - isLastParagraphEmpty = isParagraphEmpty } + // close the remaining list tags + openedListTagNames.reversed().forEach { + builder.append("") + } + return builder.toString() } @@ -444,10 +508,11 @@ internal object RichTextStateHtmlParser : RichTextStateParser { */ private fun encodeHtmlElementToRichParagraphType( tagName: String, + nestedLevel: ListNestedLevel? = null ): ParagraphType { return when (tagName) { - "ul" -> UnorderedList() - "ol" -> OrderedList(number = 1) + "ul" -> UnorderedList(initialNestedLevel = nestedLevel ?: ListNestedLevel.LEVEL_1) + "ol" -> OrderedList(number = 1, initialNestedLevel = nestedLevel ?: ListNestedLevel.LEVEL_1) else -> DefaultParagraph() } } diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/markdown/RichTextStateMarkdownParser.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/markdown/RichTextStateMarkdownParser.kt index b0f71f34..46e3e7d0 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/markdown/RichTextStateMarkdownParser.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/markdown/RichTextStateMarkdownParser.kt @@ -12,6 +12,7 @@ import com.mohamedrejeb.richeditor.model.RichSpanStyle import com.mohamedrejeb.richeditor.model.RichTextState import com.mohamedrejeb.richeditor.paragraph.RichParagraph import com.mohamedrejeb.richeditor.paragraph.type.DefaultParagraph +import com.mohamedrejeb.richeditor.paragraph.type.ListNestedLevel import com.mohamedrejeb.richeditor.paragraph.type.OrderedList import com.mohamedrejeb.richeditor.paragraph.type.ParagraphType import com.mohamedrejeb.richeditor.paragraph.type.UnorderedList @@ -38,6 +39,7 @@ internal object RichTextStateMarkdownParser : RichTextStateParser { var brParagraphIndices = mutableListOf() var currentRichSpan: RichSpan? = null var currentRichParagraphType: ParagraphType = DefaultParagraph() + var currentListNestedLevel = 0 fun onAddLineBreak() { val lastParagraph = richParagraphList.lastOrNull() @@ -114,6 +116,10 @@ internal object RichTextStateMarkdownParser : RichTextStateParser { onOpenNode = { node -> openedNodes.add(node) + if (node.type == MarkdownElementTypes.UNORDERED_LIST || node.type == MarkdownElementTypes.ORDERED_LIST) { + currentListNestedLevel++ + } + val tagSpanStyle = markdownElementsSpanStyleEncodeMap[node.type] if (node.type in markdownBlockElements) { @@ -127,7 +133,7 @@ internal object RichTextStateMarkdownParser : RichTextStateParser { // Set paragraph type if an element is a list item if (node.type == MarkdownElementTypes.LIST_ITEM) { - currentRichParagraph.type = currentRichParagraphType.getNextParagraphType() + currentRichParagraph.type = currentRichParagraphType.getNextParagraphType(ListNestedLevel.getByNumber(currentListNestedLevel)) } val newRichSpan = RichSpan(paragraph = currentRichParagraph) @@ -195,6 +201,10 @@ internal object RichTextStateMarkdownParser : RichTextStateParser { onCloseNode = { node -> openedNodes.removeLastOrNull() + if (node.type == MarkdownElementTypes.UNORDERED_LIST || node.type == MarkdownElementTypes.ORDERED_LIST) { + currentListNestedLevel-- + } + // Remove empty spans if (currentRichSpan?.isEmpty() == true) { val parent = currentRichSpan?.parent @@ -444,10 +454,10 @@ internal object RichTextStateMarkdownParser : RichTextStateParser { private fun StringBuilder.appendParagraphStartText(paragraph: RichParagraph) { when (val type = paragraph.type) { is OrderedList -> - append("${type.number}. ") + append(" ".repeat(type.nestedLevel.number -1) + "${type.number}. ") is UnorderedList -> - append("- ") + append(" ".repeat(type.nestedLevel.number -1) + "- ") else -> Unit