diff --git a/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_bold_combo_test.yaml b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_bold_combo_test.yaml new file mode 100644 index 00000000..f1faa43b --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_bold_combo_test.yaml @@ -0,0 +1,27 @@ +appId: swmansion.enriched.markdown.example +tags: + - advanced + - inline + - highlight + - bold + - text +--- +# checks highlight combined with bold (nested both ways) +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- tapOn: + id: underline-button + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + ==**Bold inside highlight**== and **==highlight inside bold==**. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'highlight_bold_combo_display' diff --git a/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_italic_combo_test.yaml b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_italic_combo_test.yaml new file mode 100644 index 00000000..a2e4194c --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_italic_combo_test.yaml @@ -0,0 +1,24 @@ +appId: swmansion.enriched.markdown.example +tags: + - advanced + - inline + - highlight + - italic + - text +--- +# checks highlight combined with italic (nested both ways) +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + ==*Italic inside highlight*== and *==highlight inside italic==*. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'highlight_italic_combo_display' diff --git a/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_spoiler_combo_test.yaml b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_spoiler_combo_test.yaml new file mode 100644 index 00000000..1cc828a3 --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_spoiler_combo_test.yaml @@ -0,0 +1,24 @@ +appId: swmansion.enriched.markdown.example +tags: + - advanced + - inline + - highlight + - spoiler + - text +--- +# checks highlight combined with spoiler (nested both ways) +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + ==||Spoiler inside highlight||== and ||==highlight inside spoiler==||. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'highlight_spoiler_combo_display' diff --git a/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_strikethrough_combo_test.yaml b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_strikethrough_combo_test.yaml new file mode 100644 index 00000000..9c19a28f --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_strikethrough_combo_test.yaml @@ -0,0 +1,24 @@ +appId: swmansion.enriched.markdown.example +tags: + - advanced + - inline + - highlight + - strikethrough + - text +--- +# checks highlight combined with strikethrough (nested both ways) +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + ==~~Strikethrough inside highlight~~== and ~~==highlight inside strikethrough==~~. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'highlight_strikethrough_combo_display' diff --git a/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_underline_combo_test.yaml b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_underline_combo_test.yaml new file mode 100644 index 00000000..5f5c807d --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/advanced/inline_elements/highlight_underline_combo_test.yaml @@ -0,0 +1,24 @@ +appId: swmansion.enriched.markdown.example +tags: + - advanced + - inline + - highlight + - underline + - text +--- +# checks highlight combined with underline (nested both ways) +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + ==__Underlined inside highlight__== and __==highlight inside underline==__. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'highlight_underline_combo_display' diff --git a/.maestro/enrichedMarkdownText/flows/basic/inline_elements/highlight_test.yaml b/.maestro/enrichedMarkdownText/flows/basic/inline_elements/highlight_test.yaml new file mode 100644 index 00000000..82450926 --- /dev/null +++ b/.maestro/enrichedMarkdownText/flows/basic/inline_elements/highlight_test.yaml @@ -0,0 +1,22 @@ +appId: swmansion.enriched.markdown.example +tags: + - smoke + - inline + - highlight + - text +--- +- launchApp + +- runFlow: + file: '../../../../subflows/move_to_playground.yaml' + +- runFlow: + file: '../../../subflows/set_enriched_text_value.yaml' + env: + VALUE: | + ==Highlighted text== and normal text and ==more highlight==. + +- runFlow: + file: '../../../subflows/capture_or_assert_screenshot.yaml' + env: + SCREENSHOT_NAME: 'highlight_display' diff --git a/.maestro/enrichedMarkdownText/screenshots/android/highlight_bold_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/highlight_bold_combo_display.png new file mode 100644 index 00000000..cf2de2ae Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/android/highlight_bold_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/android/highlight_display.png b/.maestro/enrichedMarkdownText/screenshots/android/highlight_display.png new file mode 100644 index 00000000..da80a66a Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/android/highlight_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/android/highlight_italic_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/highlight_italic_combo_display.png new file mode 100644 index 00000000..57e65417 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/android/highlight_italic_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/android/highlight_spoiler_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/highlight_spoiler_combo_display.png new file mode 100644 index 00000000..e14d032a Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/android/highlight_spoiler_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/android/highlight_strikethrough_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/highlight_strikethrough_combo_display.png new file mode 100644 index 00000000..8472dd8c Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/android/highlight_strikethrough_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/android/highlight_underline_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/android/highlight_underline_combo_display.png new file mode 100644 index 00000000..9bd2c099 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/android/highlight_underline_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_code_block_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_code_block_combo_display_diff.png new file mode 100644 index 00000000..36898b49 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_code_block_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_display_diff.png new file mode 100644 index 00000000..f14f4c76 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_image_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_image_combo_display_diff.png new file mode 100644 index 00000000..cf60d2a5 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_image_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_list_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_list_combo_display_diff.png new file mode 100644 index 00000000..4f66fcaf Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/blockquote_list_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/header_blockquote_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/header_blockquote_combo_display_diff.png new file mode 100644 index 00000000..764ad1ea Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/header_blockquote_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/header_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/header_display_diff.png new file mode 100644 index 00000000..ee4bde69 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/header_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/header_paragraph_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/header_paragraph_combo_display_diff.png new file mode 100644 index 00000000..28f3efb0 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/header_paragraph_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/highlight_bold_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_bold_combo_display.png new file mode 100644 index 00000000..ad46bc0d Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_bold_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/highlight_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_display.png new file mode 100644 index 00000000..1eb5339f Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/highlight_italic_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_italic_combo_display.png new file mode 100644 index 00000000..0e79a257 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_italic_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/highlight_spoiler_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_spoiler_combo_display.png new file mode 100644 index 00000000..f72c3223 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_spoiler_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/highlight_strikethrough_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_strikethrough_combo_display.png new file mode 100644 index 00000000..78dcc2d5 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_strikethrough_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/highlight_underline_combo_display.png b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_underline_combo_display.png new file mode 100644 index 00000000..0e0ca892 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/highlight_underline_combo_display.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/nested_blockquote_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/nested_blockquote_display_diff.png new file mode 100644 index 00000000..00e622ed Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/nested_blockquote_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_blockquote_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_blockquote_combo_display_diff.png new file mode 100644 index 00000000..6cb3631a Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_blockquote_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_code_block_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_code_block_combo_display_diff.png new file mode 100644 index 00000000..735afa0e Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_code_block_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_display_diff.png new file mode 100644 index 00000000..dbe0236a Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_list_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_list_combo_display_diff.png new file mode 100644 index 00000000..24f03b6c Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_list_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_math_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_math_combo_display_diff.png new file mode 100644 index 00000000..26568682 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_math_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_table_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_table_combo_display_diff.png new file mode 100644 index 00000000..afcff4d9 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/paragraph_table_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_combo_display_diff.png new file mode 100644 index 00000000..151e16a5 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_combo_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_display_diff.png new file mode 100644 index 00000000..bd33e094 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_display_diff.png differ diff --git a/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_image_combo_display_diff.png b/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_image_combo_display_diff.png new file mode 100644 index 00000000..e8b2fb46 Binary files /dev/null and b/.maestro/enrichedMarkdownText/screenshots/ios/thematic_break_image_combo_display_diff.png differ diff --git a/.maestro/scripts/setup-android-emulator.sh b/.maestro/scripts/setup-android-emulator.sh index 254e9281..aba12899 100755 --- a/.maestro/scripts/setup-android-emulator.sh +++ b/.maestro/scripts/setup-android-emulator.sh @@ -82,7 +82,7 @@ until adb -s "$SERIAL" shell getprop sys.boot_completed 2>/dev/null | grep -q "^ sleep 2 done -adb -s "$SERIAL" shell pm disable-user --user 0 com.google.android.inputmethod.latin +adb -s "$SERIAL" shell settings put secure show_ime_with_hard_keyboard 0 echo "Emulator ready: $AVD_NAME ($SERIAL)" echo "DEVICE_ID=$SERIAL" diff --git a/android/src/main/cpp/jni-adapter.cpp b/android/src/main/cpp/jni-adapter.cpp index a2ea1569..608e8fc9 100644 --- a/android/src/main/cpp/jni-adapter.cpp +++ b/android/src/main/cpp/jni-adapter.cpp @@ -70,6 +70,8 @@ static jint nodeTypeToJavaOrdinal(NodeType type) { return 27; case NodeType::Subscript: return 28; + case NodeType::Highlight: + return 29; default: return 0; } @@ -218,6 +220,10 @@ JNIEXPORT jobject JNICALL Java_com_swmansion_enriched_markdown_parser_Parser_nat if (subscriptField) { md4cFlags.subscript = env->GetBooleanField(flags, subscriptField) == JNI_TRUE; } + jfieldID highlightField = env->GetFieldID(flagsClass, "highlight", "Z"); + if (highlightField) { + md4cFlags.highlight = env->GetBooleanField(flags, highlightField) == JNI_TRUE; + } jfieldID permissiveAutolinksField = env->GetFieldID(flagsClass, "permissiveAutolinks", "Z"); if (permissiveAutolinksField) { md4cFlags.permissiveAutolinks = env->GetBooleanField(flags, permissiveAutolinksField) == JNI_TRUE; diff --git a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt index a6490d12..2204b42a 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt @@ -276,6 +276,7 @@ object MeasurementStore { latexMath = FeatureFlags.IS_MATH_ENABLED && props.getMapOrNull("md4cFlags").getBooleanOrDefault("latexMath", true), superscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("superscript", false), subscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("subscript", false), + highlight = props.getMapOrNull("md4cFlags").getBooleanOrDefault("highlight", false), ) val fontSize = getInitialFontSize(styleMap, context, allowFontScaling, fontScale, maxFontSizeMultiplier) @@ -355,6 +356,7 @@ object MeasurementStore { latexMath = FeatureFlags.IS_MATH_ENABLED && props.getMapOrNull("md4cFlags").getBooleanOrDefault("latexMath", true), superscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("superscript", false), subscript = props.getMapOrNull("md4cFlags").getBooleanOrDefault("subscript", false), + highlight = props.getMapOrNull("md4cFlags").getBooleanOrDefault("highlight", false), ) val allowTrailingMargin = props.getBooleanOrDefault("allowTrailingMargin", false) val fontSize = getInitialFontSize(styleMap, context, allowFontScaling, fontScale, maxFontSizeMultiplier) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt b/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt index f602ade4..c4749f30 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt @@ -36,6 +36,7 @@ data class MarkdownASTNode( Spoiler, Superscript, Subscript, + Highlight, } fun getAttribute(key: String): String? = attributes[key] diff --git a/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt b/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt index 2d869be2..f12bc1ce 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt @@ -8,6 +8,7 @@ data class Md4cFlags( val latexMath: Boolean = FeatureFlags.IS_MATH_ENABLED, val superscript: Boolean = false, val subscript: Boolean = false, + val highlight: Boolean = false, val permissiveAutolinks: Boolean = true, ) { companion object { diff --git a/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt b/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt new file mode 100644 index 00000000..a99a31f0 --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/markdown/renderer/HighlightRenderer.kt @@ -0,0 +1,25 @@ +package com.swmansion.enriched.markdown.renderer + +import android.text.SpannableStringBuilder +import com.swmansion.enriched.markdown.parser.MarkdownASTNode +import com.swmansion.enriched.markdown.spans.HighlightSpan +import com.swmansion.enriched.markdown.utils.text.span.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE + +class HighlightRenderer : NodeRenderer { + override fun render( + node: MarkdownASTNode, + builder: SpannableStringBuilder, + onLinkPress: ((String) -> Unit)?, + onLinkLongPress: ((String) -> Unit)?, + factory: RendererFactory, + ) { + factory.renderWithSpan(builder, { factory.renderChildren(node, builder, onLinkPress, onLinkLongPress) }) { start, end, blockStyle -> + builder.setSpan( + HighlightSpan(factory.styleCache, blockStyle), + start, + end, + SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE, + ) + } + } +} diff --git a/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt b/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt index c22e14b5..3f7ecb15 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt @@ -99,6 +99,7 @@ class RendererFactory( put(MarkdownASTNode.NodeType.Underline, UnderlineRenderer(config)) put(MarkdownASTNode.NodeType.Superscript, SuperscriptRenderer()) put(MarkdownASTNode.NodeType.Subscript, SubscriptRenderer()) + put(MarkdownASTNode.NodeType.Highlight, HighlightRenderer()) put(MarkdownASTNode.NodeType.Code, CodeRenderer(config)) put(MarkdownASTNode.NodeType.Image, ImageRenderer()) put(MarkdownASTNode.NodeType.LineBreak, lineBreakRenderer) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt b/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt index 5d784674..459bec07 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt @@ -44,6 +44,9 @@ class SpanStyleCache( val superscriptBaselineOffsetScale: Float = style.superscriptStyle.baselineOffsetScale val subscriptFontScale: Float = style.subscriptStyle.fontScale val subscriptBaselineOffsetScale: Float = style.subscriptStyle.baselineOffsetScale + val highlightColor: Int = style.highlightStyle.color + val highlightBackgroundColor: Int = style.highlightStyle.backgroundColor + private val paragraphColor: Int = style.paragraphStyle.color private fun buildColorsToPreserve(style: StyleConfig): IntArray { val paragraphColor = style.paragraphStyle.color @@ -75,6 +78,8 @@ class SpanStyleCache( fun getStrongColorFor(blockColor: Int): Int = strongColor ?: blockColor + fun getHighlightColorFor(blockColor: Int): Int = if (highlightColor == paragraphColor) blockColor else highlightColor + fun getEmphasisColorFor( blockColor: Int, currentColor: Int, diff --git a/android/src/main/java/com/swmansion/enriched/markdown/spans/HighlightSpan.kt b/android/src/main/java/com/swmansion/enriched/markdown/spans/HighlightSpan.kt new file mode 100644 index 00000000..9e6f315c --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/markdown/spans/HighlightSpan.kt @@ -0,0 +1,26 @@ +package com.swmansion.enriched.markdown.spans + +import android.graphics.Color +import android.text.TextPaint +import android.text.style.CharacterStyle +import com.swmansion.enriched.markdown.renderer.BlockStyle +import com.swmansion.enriched.markdown.renderer.SpanStyleCache + +/** + * Applies highlight foreground/background only. Must not reset typeface or text size so nested + * strong/emphasis spans inside ==highlight== keep working (MetricAffectingSpan would overwrite them). + */ +class HighlightSpan( + private val styleCache: SpanStyleCache, + private val blockStyle: BlockStyle, +) : CharacterStyle() { + private val foregroundColor = styleCache.getHighlightColorFor(blockStyle.color) + + override fun updateDrawState(textPaint: TextPaint) { + textPaint.color = foregroundColor + val backgroundColor = styleCache.highlightBackgroundColor + if (Color.alpha(backgroundColor) > 0) { + textPaint.bgColor = backgroundColor + } + } +} diff --git a/android/src/main/java/com/swmansion/enriched/markdown/styles/HighlightStyle.kt b/android/src/main/java/com/swmansion/enriched/markdown/styles/HighlightStyle.kt new file mode 100644 index 00000000..731b94a8 --- /dev/null +++ b/android/src/main/java/com/swmansion/enriched/markdown/styles/HighlightStyle.kt @@ -0,0 +1,19 @@ +package com.swmansion.enriched.markdown.styles + +import com.facebook.react.bridge.ReadableMap + +data class HighlightStyle( + val color: Int, + val backgroundColor: Int, +) { + companion object { + fun fromReadableMap( + map: ReadableMap, + parser: StyleParser, + ): HighlightStyle { + val color = parser.parseColor(map, "color") + val backgroundColor = parser.parseColor(map, "backgroundColor") + return HighlightStyle(color, backgroundColor) + } + } +} diff --git a/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt b/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt index 16136e3e..f1686cb6 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt @@ -257,6 +257,14 @@ class StyleConfig( SubscriptStyle.fromReadableMap(map) } + val highlightStyle: HighlightStyle by lazy { + val map = + requireNotNull(style.getMap("highlight")) { + "Highlight style not found. JS should always provide defaults." + } + HighlightStyle.fromReadableMap(map, styleParser) + } + val tableTypeface: Typeface? by lazy { val fontFamily = tableStyle.fontFamily.takeIf { it.isNotEmpty() } val fontWeight = parseFontWeight(tableStyle.fontWeight) @@ -323,7 +331,8 @@ class StyleConfig( inlineMathStyle == other.inlineMathStyle && spoilerStyle == other.spoilerStyle && superscriptStyle == other.superscriptStyle && - subscriptStyle == other.subscriptStyle + subscriptStyle == other.subscriptStyle && + highlightStyle == other.highlightStyle } override fun hashCode(): Int { @@ -349,6 +358,7 @@ class StyleConfig( result = 31 * result + spoilerStyle.hashCode() result = 31 * result + superscriptStyle.hashCode() result = 31 * result + subscriptStyle.hashCode() + result = 31 * result + highlightStyle.hashCode() return result } } diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt index a8a6cd63..a301f915 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt @@ -85,6 +85,7 @@ fun parseMd4cFlags(flags: ReadableMap?): Md4cFlags = latexMath = FeatureFlags.IS_MATH_ENABLED && (flags?.getBoolean("latexMath") ?: true), superscript = flags?.getBoolean("superscript") ?: false, subscript = flags?.getBoolean("subscript") ?: false, + highlight = flags?.getBoolean("highlight") ?: false, ) fun parseContextMenuItems(value: ReadableArray?): List = diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt index 8819011a..a16ac95c 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/common/serialization/MarkdownASTSerializer.kt @@ -67,6 +67,12 @@ object MarkdownASTSerializer { buffer.append("~") } + NodeType.Highlight -> { + buffer.append("==") + appendChildren(node, buffer) + buffer.append("==") + } + NodeType.Code -> { buffer.append("`") appendChildren(node, buffer) diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt index b7a9abde..793f7982 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt @@ -10,6 +10,7 @@ import com.swmansion.enriched.markdown.spans.CodeBlockSpan import com.swmansion.enriched.markdown.spans.CodeSpan import com.swmansion.enriched.markdown.spans.EmphasisSpan import com.swmansion.enriched.markdown.spans.HeadingSpan +import com.swmansion.enriched.markdown.spans.HighlightSpan import com.swmansion.enriched.markdown.spans.ImageSpan import com.swmansion.enriched.markdown.spans.LinkSpan import com.swmansion.enriched.markdown.spans.OrderedListSpan @@ -83,6 +84,8 @@ object HTMLGenerator { val emphasisColor: String? val strikethroughColor: String? val underlineColor: String? + val highlightColor: String + val highlightBackgroundColor: String // Image val imageMarginBottom: Int @@ -165,6 +168,8 @@ object HTMLGenerator { strikethroughColor = if (strikeColor != 0) colorToCSS(strikeColor) else null val underline = style.underlineStyle.color underlineColor = if (underline != 0) colorToCSS(underline) else null + highlightColor = colorToCSS(style.highlightStyle.color) + highlightBackgroundColor = colorToCSS(style.highlightStyle.backgroundColor) // Image val imgStyle = style.imageStyle @@ -750,6 +755,7 @@ object HTMLGenerator { val strikethroughSpans = text.getSpans(start, end, StrikethroughSpan::class.java) val linkSpans = text.getSpans(start, end, LinkSpan::class.java) val codeSpans = text.getSpans(start, end, CodeSpan::class.java) + val highlightSpans = text.getSpans(start, end, HighlightSpan::class.java) val isBold = strongSpans.isNotEmpty() || @@ -761,6 +767,21 @@ object HTMLGenerator { val isStrikethrough = strikethroughSpans.isNotEmpty() val link = linkSpans.firstOrNull() val isCode = codeSpans.isNotEmpty() && !isCodeBlock + val isHighlight = highlightSpans.isNotEmpty() + + if (isHighlight) { + html + .append("") + } link?.let { html.append("") if (isCode) html.append("") if (link != null) html.append("") + if (isHighlight) html.append("") } private fun collectParagraphs(text: Spannable): ArrayList { diff --git a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt index 1174d949..8d62d4c1 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt @@ -10,6 +10,7 @@ import com.swmansion.enriched.markdown.spans.CodeBlockSpan import com.swmansion.enriched.markdown.spans.CodeSpan import com.swmansion.enriched.markdown.spans.EmphasisSpan import com.swmansion.enriched.markdown.spans.HeadingSpan +import com.swmansion.enriched.markdown.spans.HighlightSpan import com.swmansion.enriched.markdown.spans.ImageSpan import com.swmansion.enriched.markdown.spans.LinkSpan import com.swmansion.enriched.markdown.spans.OrderedListSpan @@ -305,6 +306,7 @@ object MarkdownExtractor { val hasSuperscript = baselineShiftSpans.any { it.spanType == BaselineShiftSpan.SpanType.SUPERSCRIPT } val hasSubscript = baselineShiftSpans.any { it.spanType == BaselineShiftSpan.SpanType.SUBSCRIPT } val linkSpans = spannable.getSpans(start, end, LinkSpan::class.java) + val hasHighlight = spannable.getSpans(start, end, HighlightSpan::class.java).isNotEmpty() var result = text @@ -333,6 +335,9 @@ object MarkdownExtractor { if (linkSpans.isNotEmpty()) { result = "[$result](${linkSpans[0].url})" } + if (hasHighlight) { + result = "==$result==" + } return result } diff --git a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/inline/Highlight.stories.tsx b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/inline/Highlight.stories.tsx new file mode 100644 index 00000000..0d35a658 --- /dev/null +++ b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/inline/Highlight.stories.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { EnrichedMarkdownTextStory } from '../EnrichedMarkdownTextStory'; +import { storyMeta } from '../shared/storyMeta'; +import { + highlightStyledDefaults, + type HighlightStyleControls, +} from '../shared/storybookMarkdownStyles'; +import { + splitStyleControls, + toHighlightStyle, +} from '../shared/storybookStyleBuilders'; +import type { TextStory } from '../shared/storyTypes'; + +const MARKDOWN = + 'This is ==highlighted text== in a sentence. Combine with ==**bold**== or ==*italic*==.'; + +const argTypes = { + highlight: { + control: 'boolean', + description: 'md4cFlags.highlight — enable ==text== parsing.', + }, + color: { + control: 'color', + description: + 'markdownStyle.highlight.color — inherits block color when omitted.', + }, + backgroundColor: { + control: 'color', + description: 'markdownStyle.highlight.backgroundColor', + }, +}; + +export default storyMeta('Inline', 'Highlight'); + +export const Default: TextStory = { + args: { + markdown: MARKDOWN, + ...highlightStyledDefaults, + }, + argTypes, + render: (args) => { + const { controls, rest } = splitStyleControls( + args, + highlightStyledDefaults + ); + const { highlight, ...highlightStyle } = controls; + return ( + + ); + }, +}; diff --git a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/props/Md4cFlags.stories.tsx b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/props/Md4cFlags.stories.tsx index 45510584..f21113fe 100644 --- a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/props/Md4cFlags.stories.tsx +++ b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/props/Md4cFlags.stories.tsx @@ -11,6 +11,7 @@ type Md4cFlagsStoryExtra = { underline: boolean; superscript: boolean; subscript: boolean; + highlight: boolean; latexMath: boolean; flavor: MarkdownFlavor; }; @@ -21,6 +22,8 @@ text^superscript^ text~subscript~ +==highlight== + $E = mc^2$`; const argTypes = { @@ -38,6 +41,10 @@ const argTypes = { description: 'md4cFlags.subscript — ~text~ parsing (disables single-tilde strikethrough).', }, + highlight: { + control: 'boolean', + description: 'md4cFlags.highlight — ==text== parsing.', + }, latexMath: { control: 'boolean', description: 'md4cFlags.latexMath — $...$ and $$...$$ math parsing.', @@ -54,16 +61,24 @@ export const Default: TextStory = { underline: true, superscript: true, subscript: true, + highlight: true, latexMath: true, flavor: 'github', }, argTypes, - render: ({ underline, superscript, subscript, latexMath, ...args }) => ( + render: ({ + underline, + superscript, + subscript, + highlight, + latexMath, + ...args + }) => ( ), }; diff --git a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookMarkdownStyles.ts b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookMarkdownStyles.ts index 783b320b..beb8ad30 100644 --- a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookMarkdownStyles.ts +++ b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookMarkdownStyles.ts @@ -375,6 +375,18 @@ export const subscriptStyledDefaults: SubscriptStyleControls = { baselineOffsetScale: 0.2, }; +export type HighlightStyleControls = { + highlight: boolean; + color: string; + backgroundColor: string; +}; + +export const highlightStyledDefaults: HighlightStyleControls = { + highlight: true, + color: '#1e3a5f', + backgroundColor: '#fef08a', +}; + type NumberControlRange = { min: number; max: number; diff --git a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookStyleBuilders.ts b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookStyleBuilders.ts index 4b1ca3c8..cd7caf1f 100644 --- a/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookStyleBuilders.ts +++ b/apps/example/.rnstorybook/stories/components/EnrichedMarkdownText/shared/storybookStyleBuilders.ts @@ -16,6 +16,7 @@ import type { ParagraphStyleControls, SingleHeadingStyleControls, SpoilerStyleControls, + HighlightStyleControls, StrikethroughStyleControls, StrongStyleControls, SubscriptStyleControls, @@ -327,6 +328,15 @@ export function toSubscriptStyle( }; } +export function toHighlightStyle( + controls: Omit +): NonNullable { + return { + ...(controls.color ? { color: controls.color } : {}), + backgroundColor: controls.backgroundColor, + }; +} + export function toInlineImageStyle( controls: InlineImageStyleControls ): NonNullable { diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 60452cda..2caaae3c 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2581,4 +2581,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9c5417fc84515945aa2357a49779fde55434ae62 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/apps/example/src/screens/playground/PlaygroundScreen.tsx b/apps/example/src/screens/playground/PlaygroundScreen.tsx index 4dac70e7..c3fdeb78 100644 --- a/apps/example/src/screens/playground/PlaygroundScreen.tsx +++ b/apps/example/src/screens/playground/PlaygroundScreen.tsx @@ -47,6 +47,10 @@ const MARKDOWN_STYLE = { checkmarkColor: '#ffffff', checkedStrikethrough: true, }, + highlight: { + color: '#111827', + backgroundColor: '#FEF08A', + }, }; const BLOCK_IMAGE_URI = Image.resolveAssetSource( @@ -262,7 +266,7 @@ export default function PlaygroundScreen() { markdownStyle={MARKDOWN_STYLE} flavor="github" spoilerOverlay="solid" - md4cFlags={{ underline: underlineEnabled }} + md4cFlags={{ underline: underlineEnabled, highlight: true }} onLinkPress={({ url }) => Alert.alert('Link', url, [{ text: 'OK' }]) } diff --git a/cpp/parser/MD4CParser.cpp b/cpp/parser/MD4CParser.cpp index ebcde8b9..fd89c1db 100644 --- a/cpp/parser/MD4CParser.cpp +++ b/cpp/parser/MD4CParser.cpp @@ -316,6 +316,11 @@ class MD4CParser::Impl { break; } + case MD_SPAN_MARK: { + impl->pushNode(std::make_shared(NodeType::Highlight)); + break; + } + default: break; } @@ -493,6 +498,9 @@ std::shared_ptr MD4CParser::parse(const std::string &markdown, if (md4cFlags.subscript) { flags |= MD_FLAG_SUBSCRIPTS; } + if (md4cFlags.highlight) { + flags |= MD_FLAG_HIGHLIGHT; + } // Configure MD4C parser with callbacks MD_PARSER parser = { diff --git a/cpp/parser/MD4CParser.hpp b/cpp/parser/MD4CParser.hpp index c23916fc..7df9d36c 100644 --- a/cpp/parser/MD4CParser.hpp +++ b/cpp/parser/MD4CParser.hpp @@ -11,6 +11,7 @@ struct Md4cFlags { bool latexMath = true; bool superscript = false; bool subscript = false; + bool highlight = false; bool permissiveAutolinks = true; }; diff --git a/cpp/parser/MarkdownASTNode.hpp b/cpp/parser/MarkdownASTNode.hpp index 3c0fccd1..b1748c7d 100644 --- a/cpp/parser/MarkdownASTNode.hpp +++ b/cpp/parser/MarkdownASTNode.hpp @@ -36,7 +36,8 @@ enum class NodeType { LatexMathDisplay, Spoiler, Superscript, - Subscript + Subscript, + Highlight }; struct MarkdownASTNode { diff --git a/cpp/wasm/ASTSerializer.cpp b/cpp/wasm/ASTSerializer.cpp index 8d590063..b5bbe69a 100644 --- a/cpp/wasm/ASTSerializer.cpp +++ b/cpp/wasm/ASTSerializer.cpp @@ -64,6 +64,8 @@ static const char *nodeTypeToString(NodeType type) { return "Superscript"; case NodeType::Subscript: return "Subscript"; + case NodeType::Highlight: + return "Highlight"; default: assert(false && "unhandled NodeType in nodeTypeToString"); return ""; diff --git a/cpp/wasm/md4c_wasm.cpp b/cpp/wasm/md4c_wasm.cpp index f9c2b1dd..389be4db 100644 --- a/cpp/wasm/md4c_wasm.cpp +++ b/cpp/wasm/md4c_wasm.cpp @@ -17,9 +17,11 @@ extern "C" { * @param latexMath 1 → enable $…$ / $$…$$ LaTeX math spans; 0 → disable. * @param superscript 1 → enable ^superscript^ spans; 0 → disable. * @param subscript 1 → enable ~subscript~ spans; 0 → disable. + * @param highlight 1 → enable ==highlight== spans; 0 → disable. * @return Null-terminated UTF-8 JSON string, valid until the next call. */ -const char *parseMarkdown(const char *markdown, int underline, int latexMath, int superscript, int subscript) { +const char *parseMarkdown(const char *markdown, int underline, int latexMath, int superscript, int subscript, + int highlight) { if (!markdown) { g_resultBuffer = "{\"type\":\"Document\"}"; return g_resultBuffer.c_str(); @@ -30,6 +32,7 @@ const char *parseMarkdown(const char *markdown, int underline, int latexMath, in flags.latexMath = (latexMath != 0); flags.superscript = (superscript != 0); flags.subscript = (subscript != 0); + flags.highlight = (highlight != 0); Markdown::MD4CParser parser; auto root = parser.parse(std::string(markdown), flags); diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 24e21e96..bcda5b92 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -127,13 +127,14 @@ Configuration for md4c parser extension flags. | Type | Default Value | Platform | | ------------- | ------------------------ | -------- | -| `Md4cFlags` | `{ underline: false, superscript: false, subscript: false, latexMath: true }` | Both | +| `Md4cFlags` | `{ underline: false, superscript: false, subscript: false, highlight: false, latexMath: true }` | Both | **Properties:** - **`underline`**: When `true`, treats `_text_` as underline instead of emphasis. When enabled, only `*text*` works for italic emphasis. - **`superscript`**: When `true`, parses `^text^` as superscript. Visual appearance can be tuned with the `superscript` style prop — see [Superscript-specific](./STYLES.md#superscript-specific). - **`subscript`**: When `true`, parses `~text~` as subscript. When disabled, single and double tildes remain strikethrough markers. Visual appearance can be tuned with the `subscript` style prop — see [Subscript-specific](./STYLES.md#subscript-specific). +- **`highlight`**: When `true`, parses `==text==` as highlighted spans. When disabled, double equals signs are treated as plain text. Visual appearance can be tuned with the `highlight` style prop — see [Highlight-specific](./STYLES.md#highlight-specific). - **`latexMath`**: When `true`, parses `$...$` and `$$...$$` as LaTeX math spans. **Example:** diff --git a/docs/STYLES.md b/docs/STYLES.md index 060920cf..353fddb2 100644 --- a/docs/STYLES.md +++ b/docs/STYLES.md @@ -196,6 +196,9 @@ The library provides sensible default styles for all Markdown elements out of th fontScale: 0.75, baselineOffsetScale: 0.20, }, + highlight: { + backgroundColor: '#FEF08A', + }, }} /> ``` @@ -358,6 +361,30 @@ function App() { |----------|------|-------------| | `color` | `string` | Underline color (iOS only) | +### Highlight-specific + +Styles for highlighted text (`==text==`). Requires `md4cFlags={{ highlight: true }}` to enable the parser. Font size, family, and weight inherit from the surrounding block; only `color` and `backgroundColor` are overridden. + +| Property | Type | Description | +|----------|------|-------------| +| `color` | `string` | Text color inside the highlight. Inherits the block color when omitted | +| `backgroundColor` | `string` | Background color of the highlight span. Default: `#FEF08A` | + +```tsx + +``` + +> [!NOTE] +> When `highlight.color` is omitted, it inherits the surrounding block color. When set explicitly, it applies to the entire `==...==` span, including nested bold or italic text. Nested formatting (bold, italic, links) is preserved. + ### Image-specific | Property | Type | Description | diff --git a/ios/EnrichedMarkdown.mm b/ios/EnrichedMarkdown.mm index 50540a2c..0ffd7003 100644 --- a/ios/EnrichedMarkdown.mm +++ b/ios/EnrichedMarkdown.mm @@ -691,6 +691,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & md4cFlagsChanged = YES; _dirtyFlags |= ENRMDirtyForceHeight; } + if (newViewProps.md4cFlags.highlight != oldViewProps.md4cFlags.highlight) { + _md4cFlags.highlight = newViewProps.md4cFlags.highlight; + md4cFlagsChanged = YES; + _dirtyFlags |= ENRMDirtyForceHeight; + } BOOL allowTrailingMarginChanged = newViewProps.allowTrailingMargin != oldViewProps.allowTrailingMargin; _enableLinkPreview = newViewProps.enableLinkPreview; diff --git a/ios/EnrichedMarkdownText.mm b/ios/EnrichedMarkdownText.mm index 8acde324..1575c041 100644 --- a/ios/EnrichedMarkdownText.mm +++ b/ios/EnrichedMarkdownText.mm @@ -476,6 +476,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & md4cFlagsChanged = YES; _forceHeightUpdateOnNextRender = YES; } + if (newViewProps.md4cFlags.highlight != oldViewProps.md4cFlags.highlight) { + _md4cFlags.highlight = newViewProps.md4cFlags.highlight; + md4cFlagsChanged = YES; + _forceHeightUpdateOnNextRender = YES; + } BOOL markdownChanged = oldViewProps.markdown != newViewProps.markdown; BOOL allowTrailingMarginChanged = newViewProps.allowTrailingMargin != oldViewProps.allowTrailingMargin; diff --git a/ios/internals/MeasurementCache.h b/ios/internals/MeasurementCache.h index c2eb543f..7b4d9535 100644 --- a/ios/internals/MeasurementCache.h +++ b/ios/internals/MeasurementCache.h @@ -38,6 +38,7 @@ struct MeasurementCacheKey { bool md4cFlagsUnderline; bool md4cFlagsSuperscript; bool md4cFlagsSubscript; + bool md4cFlagsHighlight; bool md4cFlagsLatexMath; size_t styleFingerprint; CGFloat fontScale; @@ -46,12 +47,12 @@ struct MeasurementCacheKey { bool operator==(const MeasurementCacheKey &other) const { return std::tie(markdown, maxWidth, allowTrailingMargin, allowFontScaling, maxFontSizeMultiplier, - md4cFlagsUnderline, md4cFlagsSuperscript, md4cFlagsSubscript, md4cFlagsLatexMath, styleFingerprint, - fontScale, flavor) == std::tie(other.markdown, other.maxWidth, other.allowTrailingMargin, - other.allowFontScaling, other.maxFontSizeMultiplier, - other.md4cFlagsUnderline, other.md4cFlagsSuperscript, - other.md4cFlagsSubscript, other.md4cFlagsLatexMath, - other.styleFingerprint, other.fontScale, other.flavor); + md4cFlagsUnderline, md4cFlagsSuperscript, md4cFlagsSubscript, md4cFlagsHighlight, + md4cFlagsLatexMath, styleFingerprint, fontScale, flavor) == + std::tie(other.markdown, other.maxWidth, other.allowTrailingMargin, other.allowFontScaling, + other.maxFontSizeMultiplier, other.md4cFlagsUnderline, other.md4cFlagsSuperscript, + other.md4cFlagsSubscript, other.md4cFlagsHighlight, other.md4cFlagsLatexMath, + other.styleFingerprint, other.fontScale, other.flavor); } }; @@ -67,6 +68,7 @@ struct MeasurementCacheKeyHash { HashUtils::hash_one(h, key.md4cFlagsUnderline); HashUtils::hash_one(h, key.md4cFlagsSuperscript); HashUtils::hash_one(h, key.md4cFlagsSubscript); + HashUtils::hash_one(h, key.md4cFlagsHighlight); HashUtils::hash_one(h, key.md4cFlagsLatexMath); HashUtils::hash_one(h, key.styleFingerprint); HashUtils::hash_one(h, key.fontScale); @@ -117,6 +119,7 @@ template inline size_t computeStyleFingerprint(const Styl s.codeBlock.borderWidth); hashFields(s.code.fontFamily, s.code.fontSize); hashFields(s.link.fontFamily, s.strong.fontFamily, s.strong.fontWeight, s.em.fontFamily, s.em.fontStyle); + hashFields(s.highlight.backgroundColor, s.highlight.color); // Visual/Spacing Elements hashFields(s.image.height, s.image.marginTop, s.image.marginBottom); @@ -146,6 +149,7 @@ inline MeasurementCacheKey buildMeasurementCacheKey(const PropsType &props, CGFl .md4cFlagsUnderline = props.md4cFlags.underline, .md4cFlagsSuperscript = props.md4cFlags.superscript, .md4cFlagsSubscript = props.md4cFlags.subscript, + .md4cFlagsHighlight = props.md4cFlags.highlight, .md4cFlagsLatexMath = props.md4cFlags.latexMath, .styleFingerprint = computeStyleFingerprint(props.markdownStyle), .fontScale = fontScale, diff --git a/ios/parser/ENRMMarkdownParser.h b/ios/parser/ENRMMarkdownParser.h index f5505ef8..003f32dc 100644 --- a/ios/parser/ENRMMarkdownParser.h +++ b/ios/parser/ENRMMarkdownParser.h @@ -7,6 +7,7 @@ @property (nonatomic, assign) BOOL latexMath; @property (nonatomic, assign) BOOL superscript; @property (nonatomic, assign) BOOL subscript; +@property (nonatomic, assign) BOOL highlight; + (instancetype)defaultFlags; diff --git a/ios/parser/ENRMMarkdownParser.mm b/ios/parser/ENRMMarkdownParser.mm index 1ca2d3a4..a4aa92fe 100644 --- a/ios/parser/ENRMMarkdownParser.mm +++ b/ios/parser/ENRMMarkdownParser.mm @@ -12,6 +12,7 @@ - (instancetype)init _latexMath = YES; _superscript = NO; _subscript = NO; + _highlight = NO; } return self; } @@ -28,6 +29,7 @@ - (id)copyWithZone:(NSZone *)zone copy.latexMath = self.latexMath; copy.superscript = self.superscript; copy.subscript = self.subscript; + copy.highlight = self.highlight; return copy; } diff --git a/ios/parser/MarkdownASTNode.h b/ios/parser/MarkdownASTNode.h index b7c6a857..f4598dc3 100644 --- a/ios/parser/MarkdownASTNode.h +++ b/ios/parser/MarkdownASTNode.h @@ -29,7 +29,8 @@ typedef NS_ENUM(NSInteger, MarkdownNodeType) { MarkdownNodeTypeLatexMathDisplay, MarkdownNodeTypeSpoiler, MarkdownNodeTypeSuperscript, - MarkdownNodeTypeSubscript + MarkdownNodeTypeSubscript, + MarkdownNodeTypeHighlight }; @interface MarkdownASTNode : NSObject diff --git a/ios/parser/MarkdownParserBridge.mm b/ios/parser/MarkdownParserBridge.mm index 0bfbc2e4..3a762aa0 100644 --- a/ios/parser/MarkdownParserBridge.mm +++ b/ios/parser/MarkdownParserBridge.mm @@ -101,6 +101,9 @@ case Markdown::NodeType::Subscript: objcType = MarkdownNodeTypeSubscript; break; + case Markdown::NodeType::Highlight: + objcType = MarkdownNodeTypeHighlight; + break; } MarkdownASTNode *objcNode = [[MarkdownASTNode alloc] initWithType:objcType]; @@ -148,6 +151,7 @@ cppFlags.latexMath = flags.latexMath; cppFlags.superscript = flags.superscript; cppFlags.subscript = flags.subscript; + cppFlags.highlight = flags.highlight; Markdown::MD4CParser parser; auto cppAST = parser.parse(cppMarkdown, cppFlags); diff --git a/ios/renderer/HighlightRenderer.h b/ios/renderer/HighlightRenderer.h new file mode 100644 index 00000000..e743e020 --- /dev/null +++ b/ios/renderer/HighlightRenderer.h @@ -0,0 +1,7 @@ +#pragma once +#import "BaseRenderer.h" + +extern NSString *const HighlightAttributeName; + +@interface HighlightRenderer : BaseRenderer +@end diff --git a/ios/renderer/HighlightRenderer.m b/ios/renderer/HighlightRenderer.m new file mode 100644 index 00000000..089127c8 --- /dev/null +++ b/ios/renderer/HighlightRenderer.m @@ -0,0 +1,31 @@ +#import "HighlightRenderer.h" +#import "MarkdownASTNode.h" +#import "RenderContext.h" +#import "RendererFactory.h" +#import "StyleConfig.h" + +NSString *const HighlightAttributeName = @"EnrichedMarkdownHighlight"; + +@implementation HighlightRenderer + +#pragma mark - Rendering + +- (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)output context:(RenderContext *)context +{ + NSUInteger start = output.length; + [_rendererFactory renderChildrenOfNode:node into:output context:context]; + + NSRange range = NSMakeRange(start, output.length - start); + if (range.length == 0) + return; + + BlockStyle *blockStyle = [context getBlockStyle]; + RCTUIColor *foregroundColor = [RenderContext calculateHighlightColor:[_config highlightColor] + paragraphColor:[_config paragraphColor] + blockColor:blockStyle.color]; + [output addAttribute:NSForegroundColorAttributeName value:foregroundColor range:range]; + [output addAttribute:NSBackgroundColorAttributeName value:[_config highlightBackgroundColor] range:range]; + [output addAttribute:HighlightAttributeName value:@YES range:range]; +} + +@end diff --git a/ios/renderer/ParagraphRenderer.m b/ios/renderer/ParagraphRenderer.m index 26cfa02a..f9625434 100644 --- a/ios/renderer/ParagraphRenderer.m +++ b/ios/renderer/ParagraphRenderer.m @@ -48,6 +48,7 @@ - (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)out // Avoid standard line height on block images to prevent vertical alignment issues if (!isBlockImage) { applyLineHeight(output, range, _config.paragraphLineHeight); + applyBaselineOffset(output, range); } applyTextAlignment(output, range, _config.paragraphTextAlign); diff --git a/ios/renderer/RenderContext.h b/ios/renderer/RenderContext.h index c7aa2b01..860c6545 100644 --- a/ios/renderer/RenderContext.h +++ b/ios/renderer/RenderContext.h @@ -93,6 +93,13 @@ typedef NS_ENUM(NSInteger, ListType) { ListTypeUnordered, ListTypeOrdered }; */ + (RCTUIColor *)calculateStrongColor:(RCTUIColor *)configStrongColor blockColor:(RCTUIColor *)blockColor; +/** + * Resolves highlight foreground color: uses block color when config matches paragraph default. + */ ++ (RCTUIColor *)calculateHighlightColor:(RCTUIColor *)configHighlightColor + paragraphColor:(RCTUIColor *)paragraphColor + blockColor:(RCTUIColor *)blockColor; + /** * Calculates the range for content rendered between start and current output length. * Returns a range with length 0 if no content was rendered. diff --git a/ios/renderer/RenderContext.m b/ios/renderer/RenderContext.m index 68a6ff48..f3d6e8d5 100644 --- a/ios/renderer/RenderContext.m +++ b/ios/renderer/RenderContext.m @@ -277,6 +277,9 @@ + (BOOL)shouldPreserveColors:(NSDictionary *)attrs return (attrs[NSLinkAttributeName] != nil || attrs[CodeAttributeName] != nil); } +// TODO: Unify calculateStrongColor and calculateHighlightColor into a single helper that +// resolves inline foreground color from (configColor, inheritanceBaseline, blockColor). + /** * Calculates whether a strong color should override the block color. */ @@ -288,6 +291,16 @@ + (RCTUIColor *)calculateStrongColor:(RCTUIColor *)configStrongColor blockColor: return configStrongColor; } ++ (RCTUIColor *)calculateHighlightColor:(RCTUIColor *)configHighlightColor + paragraphColor:(RCTUIColor *)paragraphColor + blockColor:(RCTUIColor *)blockColor +{ + if ([configHighlightColor isEqual:paragraphColor]) { + return blockColor; + } + return configHighlightColor; +} + /** * Safely calculates a range based on a start point and the current output length. */ diff --git a/ios/renderer/RendererFactory.m b/ios/renderer/RendererFactory.m index 737fd57a..dd79f3d3 100644 --- a/ios/renderer/RendererFactory.m +++ b/ios/renderer/RendererFactory.m @@ -12,6 +12,7 @@ #endif #import "ENRMSpoilerRenderer.h" #import "HeadingRenderer.h" +#import "HighlightRenderer.h" #import "LinkRenderer.h" #import "ListItemRenderer.h" #import "ListRenderer.h" @@ -84,6 +85,8 @@ - (instancetype)initWithConfig:(StyleConfig *)config return [[SuperscriptRenderer alloc] initWithRendererFactory:self config:_config]; case MarkdownNodeTypeSubscript: return [[SubscriptRenderer alloc] initWithRendererFactory:self config:_config]; + case MarkdownNodeTypeHighlight: + return [[HighlightRenderer alloc] initWithRendererFactory:self config:_config]; case MarkdownNodeTypeParagraph: return [[ParagraphRenderer alloc] initWithRendererFactory:self config:_config]; case MarkdownNodeTypeLink: diff --git a/ios/styles/StyleConfig.h b/ios/styles/StyleConfig.h index 8b63e9dc..3191aa79 100644 --- a/ios/styles/StyleConfig.h +++ b/ios/styles/StyleConfig.h @@ -188,6 +188,11 @@ NS_ASSUME_NONNULL_END // Strikethrough properties - (RCTUIColor *)strikethroughColor; - (void)setStrikethroughColor:(RCTUIColor *)newValue; +// Highlight properties +- (nullable RCTUIColor *)highlightColor; +- (void)setHighlightColor:(nullable RCTUIColor *)newValue; +- (nullable RCTUIColor *)highlightBackgroundColor; +- (void)setHighlightBackgroundColor:(nullable RCTUIColor *)newValue; // Underline properties - (RCTUIColor *)underlineColor; - (void)setUnderlineColor:(RCTUIColor *)newValue; diff --git a/ios/styles/StyleConfig.mm b/ios/styles/StyleConfig.mm index 21f8592a..2187d0b5 100644 --- a/ios/styles/StyleConfig.mm +++ b/ios/styles/StyleConfig.mm @@ -121,6 +121,9 @@ @implementation StyleConfig { RCTUIColor *_emphasisColor; // Strikethrough properties RCTUIColor *_strikethroughColor; + // Highlight properties + RCTUIColor *_highlightColor; + RCTUIColor *_highlightBackgroundColor; // Underline properties RCTUIColor *_underlineColor; // Code properties @@ -394,6 +397,8 @@ - (id)copyWithZone:(NSZone *)zone copy->_emphasisFontStyle = [_emphasisFontStyle copy]; copy->_emphasisColor = [_emphasisColor copy]; copy->_strikethroughColor = [_strikethroughColor copy]; + copy->_highlightColor = [_highlightColor copy]; + copy->_highlightBackgroundColor = [_highlightBackgroundColor copy]; copy->_underlineColor = [_underlineColor copy]; copy->_codeFontFamily = [_codeFontFamily copy]; copy->_codeFontSize = _codeFontSize; @@ -1408,6 +1413,26 @@ - (void)setStrikethroughColor:(RCTUIColor *)newValue _strikethroughColor = newValue; } +- (RCTUIColor *)highlightColor +{ + return _highlightColor; +} + +- (void)setHighlightColor:(RCTUIColor *)newValue +{ + _highlightColor = newValue; +} + +- (RCTUIColor *)highlightBackgroundColor +{ + return _highlightBackgroundColor; +} + +- (void)setHighlightBackgroundColor:(RCTUIColor *)newValue +{ + _highlightBackgroundColor = newValue; +} + - (RCTUIColor *)underlineColor { return _underlineColor; diff --git a/ios/utils/HTMLGenerator.m b/ios/utils/HTMLGenerator.m index 34ee5c5f..686744d2 100644 --- a/ios/utils/HTMLGenerator.m +++ b/ios/utils/HTMLGenerator.m @@ -3,6 +3,7 @@ #import "CodeBackground.h" #import "ENRMFeatureFlags.h" #import "ENRMImageAttachment.h" +#import "HighlightRenderer.h" #if ENRICHED_MARKDOWN_MATH #import "ENRMMathInlineAttachment.h" #endif @@ -115,6 +116,8 @@ @interface CachedStyles : NSObject @property (nonatomic) CGFloat thematicBreakMarginBottom; @property (nonatomic, copy) NSString *strikethroughColor; @property (nonatomic, copy) NSString *underlineColor; +@property (nonatomic, copy) NSString *highlightColor; +@property (nonatomic, copy) NSString *highlightBackgroundColor; @end @implementation CachedStyles @@ -299,6 +302,8 @@ static void appendEscapedHTML(NSMutableString *output, NSString *text) cache.thematicBreakMarginBottom = [styleConfig thematicBreakMarginBottom]; cache.strikethroughColor = colorToCSS([styleConfig strikethroughColor]); cache.underlineColor = colorToCSS([styleConfig underlineColor]); + cache.highlightColor = colorToCSS([styleConfig highlightColor]); + cache.highlightBackgroundColor = colorToCSS([styleConfig highlightBackgroundColor]); return cache; } @@ -510,6 +515,19 @@ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attrib isItalic = (traits & UIFontDescriptorTraitItalic) != 0; } + BOOL isHighlight = [attrs[HighlightAttributeName] boolValue]; + + if (isHighlight) { + NSMutableString *markStyle = [NSMutableString + stringWithFormat:@"background-color: %@", styles.highlightBackgroundColor]; + if (styles.highlightColor && ![styles.highlightColor isEqualToString:styles.paragraphColor]) { + [markStyle appendFormat:@"; color: %@", styles.highlightColor]; + } else { + [markStyle appendString:@"; color: inherit"]; + } + [html appendFormat:@"", markStyle]; + } + if (linkAttr) { NSString *href = [linkAttr isKindOfClass:[NSURL class]] ? [(NSURL *)linkAttr absoluteString] : linkAttr; @@ -580,6 +598,8 @@ static void generateInlineHTML(NSMutableString *html, NSAttributedString *attrib [html appendString:@""]; if (linkAttr) [html appendString:@""]; + if (isHighlight) + [html appendString:@""]; }]; } diff --git a/ios/utils/MarkdownASTSerializer.m b/ios/utils/MarkdownASTSerializer.m index 0ee64369..74080c64 100644 --- a/ios/utils/MarkdownASTSerializer.m +++ b/ios/utils/MarkdownASTSerializer.m @@ -61,6 +61,12 @@ static void serializeNode(MarkdownASTNode *node, NSMutableString *buffer) [buffer appendString:@"~"]; break; + case MarkdownNodeTypeHighlight: + [buffer appendString:@"=="]; + serializeChildren(node, buffer); + [buffer appendString:@"=="]; + break; + case MarkdownNodeTypeCode: [buffer appendString:@"`"]; serializeChildren(node, buffer); diff --git a/ios/utils/MarkdownExtractor.m b/ios/utils/MarkdownExtractor.m index b80c78e6..eb4482c6 100644 --- a/ios/utils/MarkdownExtractor.m +++ b/ios/utils/MarkdownExtractor.m @@ -3,6 +3,7 @@ #import "ENRMFeatureFlags.h" #import "ENRMImageAttachment.h" #import "ENRMUIKit.h" +#import "HighlightRenderer.h" #include #if ENRICHED_MARKDOWN_MATH #import "ENRMMathInlineAttachment.h" @@ -80,7 +81,7 @@ static void extractFontTraits(NSDictionary *attrs, BOOL *isBold, BOOL *isItalic, static NSString *applyInlineFormatting(NSString *text, BOOL isBold, BOOL isItalic, BOOL isMonospace, BOOL isStrikethrough, BOOL isUnderline, BOOL isSuperscript, BOOL isSubscript, - NSString *linkURL) + BOOL isHighlight, NSString *linkURL) { NSMutableString *result = [NSMutableString stringWithString:text]; @@ -109,6 +110,9 @@ static void extractFontTraits(NSDictionary *attrs, BOOL *isBold, BOOL *isItalic, if (linkURL) { result = [NSMutableString stringWithFormat:@"[%@](%@)", result, linkURL]; } + if (isHighlight) { + result = [NSMutableString stringWithFormat:@"==%@==", result]; + } return result; } @@ -300,9 +304,11 @@ static void extractFontTraits(NSDictionary *attrs, BOOL *isBold, BOOL *isItalic, BOOL isSuperscript = baselineOffset != nil && [baselineOffset doubleValue] > 0; BOOL isSubscript = baselineOffset != nil && [baselineOffset doubleValue] < 0; + BOOL isHighlight = [attrs[HighlightAttributeName] boolValue]; NSString *linkURL = attrs[NSLinkAttributeName]; - NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, isStrikethrough, - isUnderline, isSuperscript, isSubscript, linkURL); + NSString *segment = + applyInlineFormatting(text, isBold, isItalic, isMonospace, isStrikethrough, isUnderline, + isSuperscript, isSubscript, isHighlight, linkURL); // Add block prefixes at line start if (isAtLineStart(result)) { diff --git a/ios/utils/ParagraphStyleUtils.h b/ios/utils/ParagraphStyleUtils.h index 40570f03..f0e1b03b 100644 --- a/ios/utils/ParagraphStyleUtils.h +++ b/ios/utils/ParagraphStyleUtils.h @@ -14,6 +14,7 @@ NSUInteger applyParagraphSpacingBefore(NSMutableAttributedString *output, NSRang NSUInteger applyBlockSpacingBefore(NSMutableAttributedString *output, NSUInteger insertionPoint, CGFloat marginTop); void applyBlockSpacingAfter(NSMutableAttributedString *output, CGFloat marginBottom); void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight); +void applyBaselineOffset(NSMutableAttributedString *output, NSRange range); void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign); NSTextAlignment textAlignmentFromString(NSString *textAlign); diff --git a/ios/utils/ParagraphStyleUtils.m b/ios/utils/ParagraphStyleUtils.m index dfedaf4c..472fad9c 100644 --- a/ios/utils/ParagraphStyleUtils.m +++ b/ios/utils/ParagraphStyleUtils.m @@ -123,6 +123,59 @@ void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat l [output addAttribute:NSParagraphStyleAttributeName value:style range:range]; } +// TODO: Extend baseline offset to every block that calls applyLineHeight (headings, blockquotes, +// code blocks, list items). Keep per-block range scoping — not a whole-document pass like RN Text, +// since blocks can use different line heights. Optionally consolidate into a single post-pass in +// AttributedRenderer; evaluate RN's per-line mode (enableIOSTextBaselineOffsetPerLine) if needed. +void applyBaselineOffset(NSMutableAttributedString *output, NSRange range) +{ + if (range.length == 0) { + return; + } + + __block CGFloat maximumLineHeight = 0; + [output enumerateAttribute:NSParagraphStyleAttributeName + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange subrange, __unused BOOL *stop) { + if (!paragraphStyle) { + return; + } + maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight); + }]; + + if (maximumLineHeight <= 0) { + return; + } + + __block CGFloat maximumFontLineHeight = 0; + [output enumerateAttribute:NSFontAttributeName + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(UIFont *font, __unused NSRange subrange, __unused BOOL *stop) { + if (!font) { + return; + } + maximumFontLineHeight = MAX(font.lineHeight, maximumFontLineHeight); + }]; + + if (maximumFontLineHeight <= 0 || maximumLineHeight < maximumFontLineHeight) { + return; + } + + CGFloat baseLineOffset = (maximumLineHeight - maximumFontLineHeight) / 2.0; + + [output enumerateAttribute:NSBaselineOffsetAttributeName + inRange:range + options:0 + usingBlock:^(NSNumber *existingOffset, NSRange subrange, BOOL *stop) { + if (existingOffset != nil) { + return; + } + [output addAttribute:NSBaselineOffsetAttributeName value:@(baseLineOffset) range:subrange]; + }]; +} + void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign) { NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location); diff --git a/ios/utils/StylePropsUtils.h b/ios/utils/StylePropsUtils.h index 16bb3379..6cd32622 100644 --- a/ios/utils/StylePropsUtils.h +++ b/ios/utils/StylePropsUtils.h @@ -613,6 +613,20 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty changed = YES; } + // ── Highlight ────────────────────────────────────────────────────────────── + + if (newStyle.highlight.color != oldStyle.highlight.color) { + RCTUIColor *highlightColor = RCTUIColorFromSharedColor(newStyle.highlight.color); + [config setHighlightColor:highlightColor]; + changed = YES; + } + + if (newStyle.highlight.backgroundColor != oldStyle.highlight.backgroundColor) { + RCTUIColor *highlightBackgroundColor = RCTUIColorFromSharedColor(newStyle.highlight.backgroundColor); + [config setHighlightBackgroundColor:highlightBackgroundColor]; + changed = YES; + } + // ── Underline ────────────────────────────────────────────────────────────── if (newStyle.underline.color != oldStyle.underline.color) { diff --git a/src/EnrichedMarkdownNativeComponent.ts b/src/EnrichedMarkdownNativeComponent.ts index 7d83fd4f..55257dca 100644 --- a/src/EnrichedMarkdownNativeComponent.ts +++ b/src/EnrichedMarkdownNativeComponent.ts @@ -172,6 +172,11 @@ interface SubscriptStyleInternal { baselineOffsetScale: CodegenTypes.Float; } +interface HighlightStyleInternal { + color: ColorValue; + backgroundColor: ColorValue; +} + export interface MarkdownStyleInternal { paragraph: ParagraphStyleInternal; h1: HeadingStyleInternal; @@ -200,6 +205,7 @@ export interface MarkdownStyleInternal { spoiler: SpoilerStyleInternal; superscript: SuperscriptStyleInternal; subscript: SubscriptStyleInternal; + highlight: HighlightStyleInternal; } export interface LinkPressEvent { @@ -261,6 +267,11 @@ export interface Md4cFlagsInternal { * @default true */ latexMath: boolean; + /** + * Enable highlight span parsing (==text==). + * @default false + */ + highlight: boolean; } interface StreamingConfigInternal { diff --git a/src/EnrichedMarkdownTextNativeComponent.ts b/src/EnrichedMarkdownTextNativeComponent.ts index 4cc5d18d..b8b40c8c 100644 --- a/src/EnrichedMarkdownTextNativeComponent.ts +++ b/src/EnrichedMarkdownTextNativeComponent.ts @@ -172,6 +172,11 @@ interface SubscriptStyleInternal { baselineOffsetScale: CodegenTypes.Float; } +interface HighlightStyleInternal { + color: ColorValue; + backgroundColor: ColorValue; +} + export interface MarkdownStyleInternal { paragraph: ParagraphStyleInternal; h1: HeadingStyleInternal; @@ -200,6 +205,7 @@ export interface MarkdownStyleInternal { spoiler: SpoilerStyleInternal; superscript: SuperscriptStyleInternal; subscript: SubscriptStyleInternal; + highlight: HighlightStyleInternal; } export interface LinkPressEvent { @@ -261,6 +267,11 @@ export interface Md4cFlagsInternal { * @default true */ latexMath: boolean; + /** + * Enable highlight span parsing (==text==). + * @default false + */ + highlight: boolean; } interface StreamingConfigInternal { diff --git a/src/native/EnrichedMarkdownText.tsx b/src/native/EnrichedMarkdownText.tsx index 8458b418..ee3d0ea8 100644 --- a/src/native/EnrichedMarkdownText.tsx +++ b/src/native/EnrichedMarkdownText.tsx @@ -32,6 +32,7 @@ const defaultMd4cFlags: Md4cFlags = { superscript: false, subscript: false, latexMath: true, + highlight: false, }; export const EnrichedMarkdownText = ({ @@ -72,6 +73,7 @@ export const EnrichedMarkdownText = ({ superscript: md4cFlags.superscript ?? false, subscript: md4cFlags.subscript ?? false, latexMath: md4cFlags.latexMath ?? true, + highlight: md4cFlags.highlight ?? false, }), [md4cFlags] ); diff --git a/src/normalizeMarkdownStyle.ts b/src/normalizeMarkdownStyle.ts index 1f0c8295..503ec8ea 100644 --- a/src/normalizeMarkdownStyle.ts +++ b/src/normalizeMarkdownStyle.ts @@ -221,6 +221,10 @@ const DEFAULT_NORMALIZED_STYLE = Object.freeze({ fontScale: Platform.select({ android: 0.65, default: 0.75 }), baselineOffsetScale: 0.2, }, + highlight: { + color: defaultTextColor, + backgroundColor: normalizeColor('#FEF08A')!, + }, }) as MarkdownStyleInternal; const refCache = new WeakMap(); @@ -289,6 +293,14 @@ export const normalizeMarkdownStyle = ( ); } + if (!style.highlight?.color) { + const paragraphColor = ( + result.paragraph as MarkdownStyleInternal['paragraph'] + ).color; + (result.highlight as MarkdownStyleInternal['highlight']).color = + paragraphColor; + } + const finalResult = Object.freeze(result) as unknown as MarkdownStyleInternal; refCache.set(style, finalResult); structuralCache.unshift({ style, result: finalResult }); diff --git a/src/normalizeMarkdownStyle.web.ts b/src/normalizeMarkdownStyle.web.ts index 14b1293a..eb27567d 100644 --- a/src/normalizeMarkdownStyle.web.ts +++ b/src/normalizeMarkdownStyle.web.ts @@ -196,6 +196,10 @@ const DEFAULT_NORMALIZED_STYLE: MarkdownStyleInternal = Object.freeze({ }, superscript: { fontScale: 0.75, baselineOffsetScale: 0.35 }, subscript: { fontScale: 0.75, baselineOffsetScale: 0.2 }, + highlight: { + color: defaultTextColor, + backgroundColor: '#FEF08A', + }, }); const refCache = new WeakMap(); @@ -260,6 +264,14 @@ export const normalizeMarkdownStyle = ( ); } + if (!style.highlight?.color) { + const paragraphColor = ( + result.paragraph as MarkdownStyleInternal['paragraph'] + ).color; + (result.highlight as MarkdownStyleInternal['highlight']).color = + paragraphColor; + } + const finalResult = Object.freeze(result) as unknown as MarkdownStyleInternal; refCache.set(style, finalResult); structuralCache.unshift({ style, result: finalResult }); diff --git a/src/types/MarkdownStyle.ts b/src/types/MarkdownStyle.ts index a509ec4a..f90e7ab1 100644 --- a/src/types/MarkdownStyle.ts +++ b/src/types/MarkdownStyle.ts @@ -220,6 +220,19 @@ interface SubscriptStyle { baselineOffsetScale?: number; } +interface HighlightStyle { + /** + * Text color inside the highlight span. + * Inherits the surrounding block color when omitted. + */ + color?: string; + /** + * Background color of the highlight span. + * @default '#FEF08A' + */ + backgroundColor?: string; +} + export interface MarkdownStyle { paragraph?: ParagraphStyle; h1?: HeadingStyle; @@ -266,6 +279,7 @@ export interface MarkdownStyle { spoiler?: SpoilerStyle; superscript?: SuperscriptStyle; subscript?: SubscriptStyle; + highlight?: HighlightStyle; } /** @@ -302,4 +316,11 @@ export interface Md4cFlags { * @default true */ latexMath?: boolean; + /** + * Enable highlight span parsing (==text==). + * When enabled, double equals are treated as highlight markers. + * When disabled, equals signs are treated as plain text. + * @default false + */ + highlight?: boolean; } diff --git a/src/types/MarkdownStyleInternal.ts b/src/types/MarkdownStyleInternal.ts index 780ab9e2..4035da56 100644 --- a/src/types/MarkdownStyleInternal.ts +++ b/src/types/MarkdownStyleInternal.ts @@ -171,6 +171,11 @@ interface SubscriptStyleInternal { baselineOffsetScale: number; } +interface HighlightStyleInternal { + color: string; + backgroundColor: string; +} + export interface MarkdownStyleInternal { paragraph: ParagraphStyleInternal; h1: HeadingStyleInternal; @@ -199,4 +204,5 @@ export interface MarkdownStyleInternal { spoiler: SpoilerStyleInternal; superscript: SuperscriptStyleInternal; subscript: SubscriptStyleInternal; + highlight: HighlightStyleInternal; } diff --git a/src/web/EnrichedMarkdownText.tsx b/src/web/EnrichedMarkdownText.tsx index 07198c7a..930d087f 100644 --- a/src/web/EnrichedMarkdownText.tsx +++ b/src/web/EnrichedMarkdownText.tsx @@ -42,6 +42,7 @@ export const EnrichedMarkdownText = ({ latexMath = true, superscript = false, subscript = false, + highlight = false, } = md4cFlags; useEffect(() => { @@ -50,7 +51,13 @@ export const EnrichedMarkdownText = ({ const katexPromise = latexMath ? loadKaTeX() : Promise.resolve(null); Promise.all([ - parseMarkdown(markdown, { underline, latexMath, superscript, subscript }), + parseMarkdown(markdown, { + underline, + latexMath, + superscript, + subscript, + highlight, + }), katexPromise, ]) .then(([result, katexInstance]) => { @@ -78,7 +85,7 @@ export const EnrichedMarkdownText = ({ return () => { cancelled = true; }; - }, [markdown, underline, latexMath, superscript, subscript]); + }, [markdown, underline, latexMath, superscript, subscript, highlight]); const callbacks = useMemo( () => ({ onLinkPress, onLinkLongPress, onTaskListItemPress }), diff --git a/src/web/parseMarkdown.ts b/src/web/parseMarkdown.ts index bbafaad2..c57f5cb4 100644 --- a/src/web/parseMarkdown.ts +++ b/src/web/parseMarkdown.ts @@ -6,7 +6,8 @@ type ParseFn = ( underline: number, latexMath: number, superscript: number, - subscript: number + subscript: number, + highlight: number ) => string; // Caching the Promise (not the resolved value) means concurrent callers share @@ -26,6 +27,7 @@ function initializeParser(): Promise { 'number', 'number', 'number', + 'number', ]) ) .catch((error) => { @@ -52,6 +54,7 @@ export async function parseMarkdown( latexMath = true, superscript = false, subscript = false, + highlight = false, }: Md4cFlags = {} ): Promise { const parse = await initializeParser(); @@ -62,7 +65,8 @@ export async function parseMarkdown( underline ? 1 : 0, latexMath ? 1 : 0, superscript ? 1 : 0, - subscript ? 1 : 0 + subscript ? 1 : 0, + highlight ? 1 : 0 ) ); diff --git a/src/web/renderers/InlineRenderers.tsx b/src/web/renderers/InlineRenderers.tsx index 0b5e4a85..55ae16fc 100644 --- a/src/web/renderers/InlineRenderers.tsx +++ b/src/web/renderers/InlineRenderers.tsx @@ -40,6 +40,10 @@ function SubscriptRenderer({ node, styles, renderChildren }: RendererProps) { return {renderChildren(node)}; } +function HighlightRenderer({ node, styles, renderChildren }: RendererProps) { + return {renderChildren(node)}; +} + function CodeRenderer({ node, styles, renderChildren }: RendererProps) { return ( {node.content ?? renderChildren(node)} @@ -110,6 +114,7 @@ export const inlineRenderers: RendererMap = { Underline: UnderlineRenderer, Superscript: SuperscriptRenderer, Subscript: SubscriptRenderer, + Highlight: HighlightRenderer, Code: CodeRenderer, Link: LinkRenderer, LatexMathInline: LatexMathInlineRenderer, diff --git a/src/web/styles.ts b/src/web/styles.ts index 5f1339e1..195eec5b 100644 --- a/src/web/styles.ts +++ b/src/web/styles.ts @@ -322,6 +322,16 @@ function subscriptStyle(style: MarkdownStyleInternal): CSSProperties { }; } +function highlightStyle(style: MarkdownStyleInternal): CSSProperties { + return { + backgroundColor: style.highlight.backgroundColor, + color: + style.highlight.color !== style.paragraph.color + ? style.highlight.color + : 'inherit', + }; +} + function mathInlineStyle(style: MarkdownStyleInternal): CSSProperties { return { color: style.inlineMath.color }; } @@ -469,6 +479,7 @@ export interface Styles { underline: CSSProperties; superscript: CSSProperties; subscript: CSSProperties; + highlight: CSSProperties; mathInline: CSSProperties; mathDisplay: CSSProperties; table: CSSProperties; @@ -513,6 +524,7 @@ export function buildStyles(style: MarkdownStyleInternal): Styles { underline: underlineStyle(style), superscript: superscriptStyle(style), subscript: subscriptStyle(style), + highlight: highlightStyle(style), mathInline: mathInlineStyle(style), mathDisplay: mathDisplayStyle(style), table: tableStyle(style), diff --git a/src/web/types.ts b/src/web/types.ts index 6cd4a971..6f8f3085 100644 --- a/src/web/types.ts +++ b/src/web/types.ts @@ -21,6 +21,7 @@ export type NodeType = | 'Underline' | 'Superscript' | 'Subscript' + | 'Highlight' | 'Code' | 'Image' | 'Blockquote' diff --git a/src/web/wasm/md4c.js b/src/web/wasm/md4c.js index de54b8d3..180e58d8 100644 Binary files a/src/web/wasm/md4c.js and b/src/web/wasm/md4c.js differ