diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/CssDecoder.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/CssDecoder.kt index e2c59c6c..2e4772e8 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/CssDecoder.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/html/CssDecoder.kt @@ -14,7 +14,8 @@ import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.isSpecified import androidx.compose.ui.unit.isUnspecified -import androidx.compose.ui.unit.sp +import com.mohamedrejeb.richeditor.parser.utils.MARK_BACKGROUND_COLOR +import com.mohamedrejeb.richeditor.parser.utils.SMALL_FONT_SIZE import com.mohamedrejeb.richeditor.utils.maxDecimals import kotlin.math.roundToInt @@ -38,22 +39,35 @@ internal object CssDecoder { * @param spanStyle the span style to decode. * @return the decoded CSS style map. */ - internal fun decodeSpanStyleToCssStyleMap(spanStyle: SpanStyle): Map { + internal fun decodeSpanStyleToHtmlStylingFormat(spanStyle: SpanStyle): HtmlStylingFormat { val cssStyleMap = mutableMapOf() + val htmlTags = mutableListOf() if (spanStyle.color.isSpecified) { cssStyleMap["color"] = decodeColorToCss(spanStyle.color) } if (spanStyle.fontSize.isSpecified) { - decodeTextUnitToCss(spanStyle.fontSize)?.let { fontSize -> - cssStyleMap["font-size"] = fontSize + if (spanStyle.fontSize == SMALL_FONT_SIZE) { + htmlTags.add("small") + } else { + decodeTextUnitToCss(spanStyle.fontSize)?.let { fontSize -> + cssStyleMap["font-size"] = fontSize + } } } spanStyle.fontWeight?.let { fontWeight -> - cssStyleMap["font-weight"] = decodeFontWeightToCss(fontWeight) + if (fontWeight == FontWeight.Bold) { + htmlTags.add("b") + } else { + cssStyleMap["font-weight"] = decodeFontWeightToCss(fontWeight) + } } spanStyle.fontStyle?.let { fontStyle -> - cssStyleMap["font-style"] = decodeFontStyleToCss(fontStyle) + if (fontStyle == FontStyle.Italic) { + htmlTags.add("i") + } else { + cssStyleMap["font-style"] = decodeFontStyleToCss(fontStyle) + } } if (spanStyle.letterSpacing.isSpecified) { decodeTextUnitToCss(spanStyle.letterSpacing)?.let { letterSpacing -> @@ -61,19 +75,40 @@ internal object CssDecoder { } } spanStyle.baselineShift?.let { baselineShift -> - cssStyleMap["baseline-shift"] = decodeBaselineShiftToCss(baselineShift) + when (baselineShift) { + BaselineShift.Subscript -> htmlTags.add("sub") + BaselineShift.Superscript -> htmlTags.add("sup") + else -> cssStyleMap["baseline-shift"] = decodeBaselineShiftToCss(baselineShift) + } } if (spanStyle.background.isSpecified) { - cssStyleMap["background"] = decodeColorToCss(spanStyle.background) + if (spanStyle.background == MARK_BACKGROUND_COLOR) { + htmlTags.add("mark") + } else { + cssStyleMap["background"] = decodeColorToCss(spanStyle.background) + } } spanStyle.textDecoration?.let { textDecoration -> - cssStyleMap["text-decoration"] = decodeTextDecorationToCss(textDecoration) + when (textDecoration) { + TextDecoration.Underline -> htmlTags.add("u") + TextDecoration.LineThrough -> htmlTags.add("s") + TextDecoration.Underline + TextDecoration.LineThrough -> { + htmlTags.add("u") + htmlTags.add("s") + } + + else -> cssStyleMap["text-decoration"] = decodeTextDecorationToCss(textDecoration) + } + } spanStyle.shadow?.let { shadow -> cssStyleMap["text-shadow"] = decodeTextShadowToCss(shadow) } - return cssStyleMap + return HtmlStylingFormat( + htmlTags = htmlTags, + cssStyleMap = cssStyleMap, + ) } /** @@ -260,4 +295,8 @@ internal object CssDecoder { } } + data class HtmlStylingFormat( + val htmlTags: List, + val cssStyleMap: Map, + ) } \ No newline at end of file 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 2a8104df..f5a2fbe6 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 @@ -213,7 +213,9 @@ internal object RichTextStateHtmlParser : RichTextStateParser { val paragraphCss = CssDecoder.decodeCssStyleMap(paragraphCssMap) // Append paragraph opening tag - builder.append("<$paragraphTagName style=\"$paragraphCss\">") + builder.append("<$paragraphTagName") + if (paragraphCss.isNotBlank()) builder.append(" style=\"$paragraphCss\"") + builder.append(">") // Append paragraph children richParagraph.children.fastForEach { richSpan -> @@ -236,7 +238,7 @@ internal object RichTextStateHtmlParser : RichTextStateParser { return builder.toString() } - private fun decodeRichSpanToHtml(richSpan: RichSpan): String { + private fun decodeRichSpanToHtml(richSpan: RichSpan, parentFormattingTags: List = emptyList()): String { val stringBuilder = StringBuilder() // Check if span is empty @@ -254,8 +256,9 @@ internal object RichTextStateHtmlParser : RichTextStateParser { } // Convert span style to CSS string - val spanCssMap = CssDecoder.decodeSpanStyleToCssStyleMap(richSpan.spanStyle) - val spanCss = CssDecoder.decodeCssStyleMap(spanCssMap) + val htmlStyleFormat = CssDecoder.decodeSpanStyleToHtmlStylingFormat(richSpan.spanStyle) + val spanCss = CssDecoder.decodeCssStyleMap(htmlStyleFormat.cssStyleMap) + val htmlTags = htmlStyleFormat.htmlTags.filter { it !in parentFormattingTags } val isRequireOpeningTag = tagName != "span" || tagAttributes.isNotEmpty() || spanCss.isNotEmpty() @@ -266,12 +269,25 @@ internal object RichTextStateHtmlParser : RichTextStateParser { stringBuilder.append(">") } + htmlTags.forEach { + stringBuilder.append("<$it>") + } + // Append text stringBuilder.append(KsoupEntities.encodeHtml(richSpan.text)) // Append children richSpan.children.fastForEach { child -> - stringBuilder.append(decodeRichSpanToHtml(child)) + stringBuilder.append( + decodeRichSpanToHtml( + richSpan = child, + parentFormattingTags = parentFormattingTags + htmlTags, + ) + ) + } + + htmlTags.reversed().forEach { + stringBuilder.append("") } if (isRequireOpeningTag) { diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/utils/ElementsSpanStyle.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/utils/ElementsSpanStyle.kt index bf95ad37..31731dde 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/utils/ElementsSpanStyle.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/parser/utils/ElementsSpanStyle.kt @@ -8,14 +8,17 @@ import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.em +internal val MARK_BACKGROUND_COLOR = Color.Yellow +internal val SMALL_FONT_SIZE = 0.8f.em + internal val BoldSpanStyle = SpanStyle(fontWeight = FontWeight.Bold) internal val ItalicSpanStyle = SpanStyle(fontStyle = FontStyle.Italic) internal val UnderlineSpanStyle = SpanStyle(textDecoration = TextDecoration.Underline) internal val StrikethroughSpanStyle = SpanStyle(textDecoration = TextDecoration.LineThrough) internal val SubscriptSpanStyle = SpanStyle(baselineShift = BaselineShift.Subscript) internal val SuperscriptSpanStyle = SpanStyle(baselineShift = BaselineShift.Superscript) -internal val MarkSpanStyle = SpanStyle(background = Color.Yellow) -internal val SmallSpanStyle = SpanStyle(fontSize = 0.8f.em) +internal val MarkSpanStyle = SpanStyle(background = MARK_BACKGROUND_COLOR) +internal val SmallSpanStyle = SpanStyle(fontSize = SMALL_FONT_SIZE) internal val H1SPanStyle = SpanStyle(fontSize = 2.em, fontWeight = FontWeight.Bold) internal val H2SPanStyle = SpanStyle(fontSize = 1.5.em, fontWeight = FontWeight.Bold) internal val H3SPanStyle = SpanStyle(fontSize = 1.17.em, fontWeight = FontWeight.Bold)