react-native-enriched-markdown-scaffold 0.7.4-scaffold.1 → 0.7.4-scaffold.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/android/build.gradle +5 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +1 -0
  4. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +5 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +49 -0
  6. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +6 -4
  7. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +20 -0
  8. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +55 -9
  9. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockSyntaxSentinel.kt +69 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +71 -12
  11. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt +30 -2
  12. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +7 -2
  13. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +3 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +1 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +8 -1
  16. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -1
  17. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +5 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathFontWeight.kt +25 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +12 -1
  20. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +7 -7
  21. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +10 -0
  22. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/SpannableExtensions.kt +12 -4
  23. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +1 -1
  24. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +2 -0
  25. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
  26. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlinePlaceholderSpan.kt +7 -3
  27. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +8 -5
  28. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathTypeface.kt +30 -0
  29. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  30. package/docs/API_REFERENCE.md +9 -0
  31. package/docs/INPUT.md +48 -0
  32. package/docs/LATEX_MATH.md +62 -0
  33. package/docs/STYLES.md +3 -0
  34. package/ios/EnrichedMarkdown.mm +44 -12
  35. package/ios/EnrichedMarkdownText.mm +47 -11
  36. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +5 -1
  37. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  38. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  39. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +5 -0
  40. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +49 -0
  41. package/ios/input/ENRMBlockSyntaxSentinel.h +25 -0
  42. package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
  43. package/ios/input/ENRMInputFormatter.h +4 -0
  44. package/ios/input/ENRMInputFormatter.mm +2 -0
  45. package/ios/input/ENRMInputParser.h +5 -0
  46. package/ios/input/ENRMInputParser.mm +79 -6
  47. package/ios/input/EnrichedMarkdownTextInput.mm +55 -22
  48. package/ios/input/InputStylePropsUtils.h +13 -0
  49. package/ios/internals/MeasurementCache.h +1 -0
  50. package/ios/math/ENRMRaTeXBridge.swift +52 -3
  51. package/ios/parser/ENRMMarkdownParser.h +8 -0
  52. package/ios/parser/ENRMMarkdownParser.mm +11 -0
  53. package/ios/renderer/ENRMMathInlineRenderer.m +3 -0
  54. package/ios/renderer/ListItemRenderer.m +8 -5
  55. package/ios/styles/StyleConfig.h +6 -0
  56. package/ios/styles/StyleConfig.mm +36 -0
  57. package/ios/utils/ENRMMathFontWeight.h +18 -0
  58. package/ios/utils/StylePropsUtils.h +21 -0
  59. package/ios/views/ENRMMathContainerView.m +3 -1
  60. package/lib/module/EnrichedMarkdownNativeComponent.ts +3 -0
  61. package/lib/module/EnrichedMarkdownTextInput.js +2 -0
  62. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  63. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +12 -0
  64. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +3 -0
  65. package/lib/module/normalizeMarkdownStyle.js +5 -2
  66. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  67. package/lib/module/normalizeMarkdownStyle.web.js +5 -2
  68. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  69. package/lib/module/normalizeMarkdownTextInputStyle.js +7 -3
  70. package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
  71. package/lib/module/web/globalStyles.js +8 -1
  72. package/lib/module/web/globalStyles.js.map +1 -1
  73. package/lib/module/web/styles.js +3 -1
  74. package/lib/module/web/styles.js.map +1 -1
  75. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +3 -0
  76. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  77. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +3 -1
  78. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  79. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +12 -0
  80. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  81. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +3 -0
  82. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  83. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  84. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  85. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +1 -0
  86. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
  87. package/lib/typescript/src/types/MarkdownStyle.d.ts +32 -0
  88. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  89. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +3 -0
  90. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  91. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  92. package/package.json +1 -1
  93. package/src/EnrichedMarkdownNativeComponent.ts +3 -0
  94. package/src/EnrichedMarkdownTextInput.tsx +4 -0
  95. package/src/EnrichedMarkdownTextInputNativeComponent.ts +12 -0
  96. package/src/EnrichedMarkdownTextNativeComponent.ts +3 -0
  97. package/src/normalizeMarkdownStyle.ts +6 -1
  98. package/src/normalizeMarkdownStyle.web.ts +6 -1
  99. package/src/normalizeMarkdownTextInputStyle.ts +5 -0
  100. package/src/types/MarkdownStyle.ts +32 -0
  101. package/src/types/MarkdownStyleInternal.ts +3 -0
  102. package/src/web/globalStyles.ts +7 -0
  103. package/src/web/styles.ts +5 -1
@@ -67,6 +67,10 @@ android {
67
67
  }
68
68
  }
69
69
  }
70
+
71
+ testOptions {
72
+ unitTests.includeAndroidResources = true
73
+ }
70
74
  }
71
75
 
72
76
  repositories {
@@ -85,6 +89,7 @@ dependencies {
85
89
  if (enableMath) {
86
90
  implementation("io.github.erweixin:ratex-android:0.1.10")
87
91
  }
92
+ testImplementation "junit:junit:4.13.2"
88
93
  }
89
94
 
90
95
  react {
@@ -44,6 +44,9 @@ public class EnrichedMarkdownTextInputManagerDelegate<T extends View, U extends
44
44
  case "scrollEnabled":
45
45
  mViewManager.setScrollEnabled(view, value == null ? false : (boolean) value);
46
46
  break;
47
+ case "blockSyntax":
48
+ mViewManager.setBlockSyntax(view, value == null ? false : (boolean) value);
49
+ break;
47
50
  case "autoCapitalize":
48
51
  mViewManager.setAutoCapitalize(view, value == null ? null : (String) value);
49
52
  break;
@@ -22,6 +22,7 @@ public interface EnrichedMarkdownTextInputManagerInterface<T extends View> exten
22
22
  void setEditable(T view, boolean value);
23
23
  void setAutoFocus(T view, boolean value);
24
24
  void setScrollEnabled(T view, boolean value);
25
+ void setBlockSyntax(T view, boolean value);
25
26
  void setAutoCapitalize(T view, @Nullable String value);
26
27
  void setMultiline(T view, boolean value);
27
28
  void setCursorColor(T view, @Nullable Integer value);
@@ -144,6 +144,7 @@ EnrichedMarkdownTextInputProps::EnrichedMarkdownTextInputProps(
144
144
  editable(convertRawProp(context, rawProps, "editable", sourceProps.editable, {false})),
145
145
  autoFocus(convertRawProp(context, rawProps, "autoFocus", sourceProps.autoFocus, {false})),
146
146
  scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
147
+ blockSyntax(convertRawProp(context, rawProps, "blockSyntax", sourceProps.blockSyntax, {false})),
147
148
  autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
148
149
  multiline(convertRawProp(context, rawProps, "multiline", sourceProps.multiline, {false})),
149
150
  cursorColor(convertRawProp(context, rawProps, "cursorColor", sourceProps.cursorColor, {})),
@@ -202,6 +203,10 @@ folly::dynamic EnrichedMarkdownTextInputProps::getDiffProps(
202
203
  result["scrollEnabled"] = scrollEnabled;
203
204
  }
204
205
 
206
+ if (blockSyntax != oldProps->blockSyntax) {
207
+ result["blockSyntax"] = blockSyntax;
208
+ }
209
+
205
210
  if (autoCapitalize != oldProps->autoCapitalize) {
206
211
  result["autoCapitalize"] = autoCapitalize;
207
212
  }
@@ -1534,6 +1534,7 @@ struct EnrichedMarkdownMarkdownStyleMathStruct {
1534
1534
  Float marginTop{0.0};
1535
1535
  Float marginBottom{0.0};
1536
1536
  std::string textAlign{};
1537
+ std::string fontWeight{};
1537
1538
 
1538
1539
 
1539
1540
  #ifdef RN_SERIALIZABLE_STATE
@@ -1548,6 +1549,7 @@ struct EnrichedMarkdownMarkdownStyleMathStruct {
1548
1549
  result["marginTop"] = marginTop;
1549
1550
  result["marginBottom"] = marginBottom;
1550
1551
  result["textAlign"] = textAlign;
1552
+ result["fontWeight"] = fontWeight;
1551
1553
  return result;
1552
1554
  }
1553
1555
  #endif
@@ -1584,6 +1586,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
1584
1586
  if (tmp_textAlign != map.end()) {
1585
1587
  fromRawValue(context, tmp_textAlign->second, result.textAlign);
1586
1588
  }
1589
+ auto tmp_fontWeight = map.find("fontWeight");
1590
+ if (tmp_fontWeight != map.end()) {
1591
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
1592
+ }
1587
1593
  }
1588
1594
 
1589
1595
  static inline std::string toString(const EnrichedMarkdownMarkdownStyleMathStruct &value) {
@@ -1598,6 +1604,8 @@ static inline folly::dynamic toDynamic(const EnrichedMarkdownMarkdownStyleMathSt
1598
1604
 
1599
1605
  struct EnrichedMarkdownMarkdownStyleInlineMathStruct {
1600
1606
  SharedColor color{};
1607
+ Float paddingVertical{0.0};
1608
+ std::string fontWeight{};
1601
1609
 
1602
1610
 
1603
1611
  #ifdef RN_SERIALIZABLE_STATE
@@ -1606,6 +1614,8 @@ struct EnrichedMarkdownMarkdownStyleInlineMathStruct {
1606
1614
  folly::dynamic toDynamic() const {
1607
1615
  folly::dynamic result = folly::dynamic::object();
1608
1616
  result["color"] = ::facebook::react::toDynamic(color);
1617
+ result["paddingVertical"] = paddingVertical;
1618
+ result["fontWeight"] = fontWeight;
1609
1619
  return result;
1610
1620
  }
1611
1621
  #endif
@@ -1618,6 +1628,14 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
1618
1628
  if (tmp_color != map.end()) {
1619
1629
  fromRawValue(context, tmp_color->second, result.color);
1620
1630
  }
1631
+ auto tmp_paddingVertical = map.find("paddingVertical");
1632
+ if (tmp_paddingVertical != map.end()) {
1633
+ fromRawValue(context, tmp_paddingVertical->second, result.paddingVertical);
1634
+ }
1635
+ auto tmp_fontWeight = map.find("fontWeight");
1636
+ if (tmp_fontWeight != map.end()) {
1637
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
1638
+ }
1621
1639
  }
1622
1640
 
1623
1641
  static inline std::string toString(const EnrichedMarkdownMarkdownStyleInlineMathStruct &value) {
@@ -2804,6 +2822,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathStruct {
2804
2822
  Float paddingVertical{0.0};
2805
2823
  SharedColor selectedBackgroundColor{};
2806
2824
  SharedColor selectedBorderColor{};
2825
+ std::string fontWeight{};
2807
2826
 
2808
2827
 
2809
2828
  #ifdef RN_SERIALIZABLE_STATE
@@ -2821,6 +2840,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathStruct {
2821
2840
  result["paddingVertical"] = paddingVertical;
2822
2841
  result["selectedBackgroundColor"] = ::facebook::react::toDynamic(selectedBackgroundColor);
2823
2842
  result["selectedBorderColor"] = ::facebook::react::toDynamic(selectedBorderColor);
2843
+ result["fontWeight"] = fontWeight;
2824
2844
  return result;
2825
2845
  }
2826
2846
  #endif
@@ -2869,6 +2889,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2869
2889
  if (tmp_selectedBorderColor != map.end()) {
2870
2890
  fromRawValue(context, tmp_selectedBorderColor->second, result.selectedBorderColor);
2871
2891
  }
2892
+ auto tmp_fontWeight = map.find("fontWeight");
2893
+ if (tmp_fontWeight != map.end()) {
2894
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
2895
+ }
2872
2896
  }
2873
2897
 
2874
2898
  static inline std::string toString(const EnrichedMarkdownTextInputMarkdownStyleMathStruct &value) {
@@ -2892,6 +2916,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathBlockStruct {
2892
2916
  Float paddingVertical{0.0};
2893
2917
  SharedColor selectedBackgroundColor{};
2894
2918
  SharedColor selectedBorderColor{};
2919
+ std::string fontWeight{};
2895
2920
  Float marginTop{0.0};
2896
2921
  Float marginBottom{0.0};
2897
2922
  std::string alignment{};
@@ -2912,6 +2937,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathBlockStruct {
2912
2937
  result["paddingVertical"] = paddingVertical;
2913
2938
  result["selectedBackgroundColor"] = ::facebook::react::toDynamic(selectedBackgroundColor);
2914
2939
  result["selectedBorderColor"] = ::facebook::react::toDynamic(selectedBorderColor);
2940
+ result["fontWeight"] = fontWeight;
2915
2941
  result["marginTop"] = marginTop;
2916
2942
  result["marginBottom"] = marginBottom;
2917
2943
  result["alignment"] = alignment;
@@ -2963,6 +2989,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2963
2989
  if (tmp_selectedBorderColor != map.end()) {
2964
2990
  fromRawValue(context, tmp_selectedBorderColor->second, result.selectedBorderColor);
2965
2991
  }
2992
+ auto tmp_fontWeight = map.find("fontWeight");
2993
+ if (tmp_fontWeight != map.end()) {
2994
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
2995
+ }
2966
2996
  auto tmp_marginTop = map.find("marginTop");
2967
2997
  if (tmp_marginTop != map.end()) {
2968
2998
  fromRawValue(context, tmp_marginTop->second, result.marginTop);
@@ -3329,6 +3359,7 @@ class EnrichedMarkdownTextInputProps final : public ViewProps {
3329
3359
  bool editable{false};
3330
3360
  bool autoFocus{false};
3331
3361
  bool scrollEnabled{false};
3362
+ bool blockSyntax{false};
3332
3363
  std::string autoCapitalize{};
3333
3364
  bool multiline{false};
3334
3365
  SharedColor cursorColor{};
@@ -4872,6 +4903,7 @@ struct EnrichedMarkdownTextMarkdownStyleMathStruct {
4872
4903
  Float marginTop{0.0};
4873
4904
  Float marginBottom{0.0};
4874
4905
  std::string textAlign{};
4906
+ std::string fontWeight{};
4875
4907
 
4876
4908
 
4877
4909
  #ifdef RN_SERIALIZABLE_STATE
@@ -4886,6 +4918,7 @@ struct EnrichedMarkdownTextMarkdownStyleMathStruct {
4886
4918
  result["marginTop"] = marginTop;
4887
4919
  result["marginBottom"] = marginBottom;
4888
4920
  result["textAlign"] = textAlign;
4921
+ result["fontWeight"] = fontWeight;
4889
4922
  return result;
4890
4923
  }
4891
4924
  #endif
@@ -4922,6 +4955,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
4922
4955
  if (tmp_textAlign != map.end()) {
4923
4956
  fromRawValue(context, tmp_textAlign->second, result.textAlign);
4924
4957
  }
4958
+ auto tmp_fontWeight = map.find("fontWeight");
4959
+ if (tmp_fontWeight != map.end()) {
4960
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
4961
+ }
4925
4962
  }
4926
4963
 
4927
4964
  static inline std::string toString(const EnrichedMarkdownTextMarkdownStyleMathStruct &value) {
@@ -4936,6 +4973,8 @@ static inline folly::dynamic toDynamic(const EnrichedMarkdownTextMarkdownStyleMa
4936
4973
 
4937
4974
  struct EnrichedMarkdownTextMarkdownStyleInlineMathStruct {
4938
4975
  SharedColor color{};
4976
+ Float paddingVertical{0.0};
4977
+ std::string fontWeight{};
4939
4978
 
4940
4979
 
4941
4980
  #ifdef RN_SERIALIZABLE_STATE
@@ -4944,6 +4983,8 @@ struct EnrichedMarkdownTextMarkdownStyleInlineMathStruct {
4944
4983
  folly::dynamic toDynamic() const {
4945
4984
  folly::dynamic result = folly::dynamic::object();
4946
4985
  result["color"] = ::facebook::react::toDynamic(color);
4986
+ result["paddingVertical"] = paddingVertical;
4987
+ result["fontWeight"] = fontWeight;
4947
4988
  return result;
4948
4989
  }
4949
4990
  #endif
@@ -4956,6 +4997,14 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
4956
4997
  if (tmp_color != map.end()) {
4957
4998
  fromRawValue(context, tmp_color->second, result.color);
4958
4999
  }
5000
+ auto tmp_paddingVertical = map.find("paddingVertical");
5001
+ if (tmp_paddingVertical != map.end()) {
5002
+ fromRawValue(context, tmp_paddingVertical->second, result.paddingVertical);
5003
+ }
5004
+ auto tmp_fontWeight = map.find("fontWeight");
5005
+ if (tmp_fontWeight != map.end()) {
5006
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
5007
+ }
4959
5008
  }
4960
5009
 
4961
5010
  static inline std::string toString(const EnrichedMarkdownTextMarkdownStyleInlineMathStruct &value) {
@@ -25,6 +25,7 @@ import com.swmansion.enriched.markdown.utils.common.RenderedSegment
25
25
  import com.swmansion.enriched.markdown.utils.common.StreamingMarkdownFilter
26
26
  import com.swmansion.enriched.markdown.utils.common.TableStreamingMode
27
27
  import com.swmansion.enriched.markdown.utils.common.getBooleanOrDefault
28
+ import com.swmansion.enriched.markdown.utils.common.getFloatOrDefault
28
29
  import com.swmansion.enriched.markdown.utils.common.getMapOrNull
29
30
  import com.swmansion.enriched.markdown.utils.common.getStringOrDefault
30
31
  import com.swmansion.enriched.markdown.utils.common.splitASTIntoSegments
@@ -299,7 +300,6 @@ object MeasurementStore {
299
300
 
300
301
  // 2. Render & Measure
301
302
  val spannable = tryRenderMarkdown(markdown, styleMap, context, md4cFlags, allowFontScaling, maxFontSizeMultiplier)
302
- spannable?.replaceMathSpansWithPlaceholders(context)
303
303
  val textToMeasure = spannable ?: markdown
304
304
  val (size, _) = measureWithLayout(width, textToMeasure, measurePaint, id)
305
305
 
@@ -420,7 +420,7 @@ object MeasurementStore {
420
420
 
421
421
  when (segment) {
422
422
  is RenderedSegment.Text -> {
423
- segment.styledText.replaceMathSpansWithPlaceholders(context)
423
+ segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.paddingVertical)
424
424
 
425
425
  val layout = createStaticLayout(segment.styledText, fontSize, widthPx, id)
426
426
  totalHeightPx += layout.height
@@ -503,7 +503,9 @@ object MeasurementStore {
503
503
  val ast = Parser.shared.parseMarkdown(markdown, md4cFlags) ?: return null
504
504
  val style = StyleConfig(styleMap, context, allowFontScaling, maxFontSizeMultiplier)
505
505
  measureRenderer.configure(style, context)
506
- measureRenderer.renderDocument(ast, null)
506
+ measureRenderer.renderDocument(ast, null).apply {
507
+ replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.paddingVertical)
508
+ }
507
509
  } catch (e: Exception) {
508
510
  Log.w(TAG, "Failed to render markdown for measurement, falling back to raw text", e)
509
511
  null
@@ -517,7 +519,7 @@ object MeasurementStore {
517
519
  fontScale: Float,
518
520
  maxFontSizeMultiplier: Float,
519
521
  ): Float {
520
- val fontSizeSp = styleMap?.getMap("paragraph")?.getDouble("fontSize")?.toFloat() ?: 16f
522
+ val fontSizeSp = styleMap.getMapOrNull("paragraph").getFloatOrDefault("fontSize", 16f)
521
523
  val density = context.resources.displayMetrics.density
522
524
 
523
525
  if (!allowFontScaling) {
@@ -57,6 +57,13 @@ class EnrichedMarkdownTextInputManager :
57
57
  stateWrapper: StateWrapper?,
58
58
  ): Any? {
59
59
  view.stateWrapper = stateWrapper
60
+ // Fabric applies props before it hands over the state wrapper, so every layout
61
+ // invalidation raised during the mount transaction was dropped. Replay it now that we can
62
+ // actually push state — otherwise the shadow node keeps the initialMeasure() estimate,
63
+ // which ignores math chips, until the first keystroke.
64
+ if (stateWrapper != null) {
65
+ view.layoutManager.flushPendingInvalidation()
66
+ }
60
67
  return super.updateState(view, props, stateWrapper)
61
68
  }
62
69
 
@@ -69,6 +76,11 @@ class EnrichedMarkdownTextInputManager :
69
76
  view.dismissActiveMention()
70
77
  super.onDropViewInstance(view)
71
78
  view.layoutManager.release()
79
+ // Unreachable while view recycling is off — it needs a `setupViewRecycling()` call from this
80
+ // constructor *and* ReactNativeFeatureFlags.enableViewRecycling(). Kept because the flush
81
+ // above turns a recycled view into a view that pushes state at mount: without this, it would
82
+ // push into the shadow node it was dropped from.
83
+ view.stateWrapper = null
72
84
  }
73
85
 
74
86
  override fun measure(
@@ -158,6 +170,14 @@ class EnrichedMarkdownTextInputManager :
158
170
  view?.isVerticalScrollBarEnabled = value
159
171
  }
160
172
 
173
+ @ReactProp(name = "blockSyntax", defaultBoolean = true)
174
+ override fun setBlockSyntax(
175
+ view: EnrichedMarkdownTextInputView?,
176
+ value: Boolean,
177
+ ) {
178
+ view?.blockSyntax = value
179
+ }
180
+
161
181
  @ReactProp(name = "autoCapitalize")
162
182
  override fun setAutoCapitalize(
163
183
  view: EnrichedMarkdownTextInputView?,
@@ -97,6 +97,17 @@ class EnrichedMarkdownTextInputView(
97
97
  private var detectScrollMovement = false
98
98
  var scrollEnabled: Boolean = true
99
99
 
100
+ // The markdown last handed over by JS, kept so `blockSyntax` can re-parse it. @ReactProp
101
+ // setters fire in undefined order, so `defaultValue` regularly lands before the flag does.
102
+ private var lastMarkdownFromJS: String? = null
103
+
104
+ var blockSyntax: Boolean = true
105
+ set(value) {
106
+ if (field == value) return
107
+ field = value
108
+ lastMarkdownFromJS?.let { setValueFromJS(it) }
109
+ }
110
+
100
111
  private var mentionIndicators: LinkedHashSet<String> = linkedSetOf()
101
112
  private var activeMentionIndicator: String? = null
102
113
  private var activeMentionStart = -1
@@ -634,7 +645,8 @@ class EnrichedMarkdownTextInputView(
634
645
  }
635
646
 
636
647
  fun setValueFromJS(markdown: String) {
637
- val parsed = InputParser.parseToPlainTextAndRanges(markdown)
648
+ lastMarkdownFromJS = markdown
649
+ val parsed = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax)
638
650
  blockEmitting = true
639
651
  try {
640
652
  runAsATransaction {
@@ -708,8 +720,16 @@ class EnrichedMarkdownTextInputView(
708
720
  }
709
721
  invalidate()
710
722
  requestLayout()
723
+ // Chips only reach the text here when `defaultValue` landed before `markdownStyle`
724
+ // (@ReactProp order is undefined). requestLayout() cannot change a height Fabric owns —
725
+ // the shadow tree has to re-measure text that now carries the spans.
726
+ layoutManager.invalidateLayout()
711
727
  } else if (mathStyleChanged) {
712
728
  refreshAllMathChips()
729
+ // A math font size / padding change resizes every chip, so the measured height moves.
730
+ // Colour-only changes end up here too; store() then measures the same size and skips
731
+ // the state update.
732
+ layoutManager.invalidateLayout()
713
733
  }
714
734
  return changed
715
735
  }
@@ -848,18 +868,36 @@ class EnrichedMarkdownTextInputView(
848
868
  return true
849
869
  }
850
870
 
851
- private fun computeChipBounds(
871
+ /** Chip glyph rect in [Layout] coordinates — no padding, no scroll offset. */
872
+ private fun chipBoundsInLayout(
852
873
  layout: Layout,
853
874
  start: Int,
854
875
  ): Rect {
855
876
  val line = layout.getLineForOffset(start)
856
- val xStart = (layout.getPrimaryHorizontal(start)).toInt() + totalPaddingLeft - scrollX
857
- val xEnd = (layout.getPrimaryHorizontal(start + 1)).toInt() + totalPaddingLeft - scrollX
858
- val top = layout.getLineTop(line) + totalPaddingTop - scrollY
859
- val bottom = layout.getLineBottom(line) + totalPaddingTop - scrollY
860
- return Rect(xStart, top, xEnd, bottom)
877
+ val xStart = layout.getPrimaryHorizontal(start).toInt()
878
+ val xEnd = layout.getPrimaryHorizontal(start + 1).toInt()
879
+ // Ordered, not raw start/end: in an RTL run xStart > xEnd, which would make an
880
+ // inverted Rect that contains() always rejects.
881
+ val left = xStart.coerceAtMost(xEnd)
882
+ var right = xStart.coerceAtLeast(xEnd)
883
+ if (right <= left) {
884
+ // A chip that is the last character has start + 1 == length, where some layouts
885
+ // report the same horizontal for both offsets. Fall back to the line's content edge
886
+ // so the rect keeps a real width instead of collapsing.
887
+ right = layout.getLineRight(line).toInt()
888
+ }
889
+ return Rect(left, layout.getLineTop(line), right, layout.getLineBottom(line))
861
890
  }
862
891
 
892
+ /** Chip glyph rect in view coordinates, for reporting to JS. */
893
+ private fun computeChipBounds(
894
+ layout: Layout,
895
+ start: Int,
896
+ ): Rect =
897
+ chipBoundsInLayout(layout, start).apply {
898
+ offset(totalPaddingLeft - scrollX, totalPaddingTop - scrollY)
899
+ }
900
+
863
901
  fun mathChipPositionAtTouch(
864
902
  x: Float,
865
903
  y: Float,
@@ -869,13 +907,21 @@ class EnrichedMarkdownTextInputView(
869
907
  val layoutNow = layout ?: return -1
870
908
  val adjustedX = (x - totalPaddingLeft + scrollX).toInt()
871
909
  val adjustedY = (y - totalPaddingTop + scrollY).toInt()
872
- if (adjustedY < 0) return -1
910
+ // getLineForVertical and getOffsetForHorizontal both clamp into the laid-out text, so
911
+ // a touch in the padding or in the blank space below the last line resolves onto the
912
+ // nearest character. Bound the touch to the text box, then require it to land inside
913
+ // the chip's own rect — otherwise a tap beside a chip reads as a tap on it.
914
+ if (adjustedY < 0 || adjustedY > layoutNow.height) return -1
873
915
  val line = layoutNow.getLineForVertical(adjustedY)
874
916
  val offset = layoutNow.getOffsetForHorizontal(line, adjustedX.toFloat())
875
- if (offset < 0 || offset >= editable.length) return -1
917
+ if (offset < 0) return -1
876
918
  val candidates = intArrayOf(offset, offset - 1).filter { it in 0 until editable.length }
877
919
  for (candidate in candidates) {
878
920
  val range = controller.chipRangeAt(editable, candidate) ?: continue
921
+ // An empty rect means the layout could not give us the chip's extent; accept the
922
+ // candidate then, rather than making the chip untappable.
923
+ val bounds = chipBoundsInLayout(layoutNow, range.first)
924
+ if (!bounds.isEmpty && !bounds.contains(adjustedX, adjustedY)) continue
879
925
  return range.first
880
926
  }
881
927
  return -1
@@ -0,0 +1,69 @@
1
+ package com.swmansion.enriched.markdown.input.formatting
2
+
3
+ /**
4
+ * Hides block-level markdown structure from md4c so a document parses as a single
5
+ * `Paragraph`. Used by `blockSyntax = false` on `EnrichedMarkdownTextInput`.
6
+ *
7
+ * Every newline is swapped for a vertical tab (`0x0B`) and one more is prepended.
8
+ * md4c treats VT as ordinary whitespace, never as a line break, so no line ever
9
+ * starts at a position where a list marker, `#`, `>` or an indented code block can
10
+ * be recognised — including line 1, which is what the leading sentinel is for.
11
+ * Inline syntax (`$...$`, `**...**`, links) still parses normally.
12
+ *
13
+ * The transform is 1:1 in characters *and* in UTF-8 bytes (VT and `\n` are both one
14
+ * of each), so offsets coming back out of the parser only need a flat -1 shift for
15
+ * the leading sentinel. iOS relies on the same property to keep its byte-to-UTF16
16
+ * map valid.
17
+ */
18
+ internal object BlockSyntaxSentinel {
19
+ const val SENTINEL = '\u000B'
20
+
21
+ /**
22
+ * `\r\n` / `\r` collapse to `\n` (md4c normalizes line endings this way regardless,
23
+ * so this is pre-existing lossiness, not a new one), and any `0x0B` already present
24
+ * is dropped — leaving it in would make [decode] silently turn it into a newline.
25
+ */
26
+ fun canonicalize(markdown: String): String {
27
+ val out = StringBuilder(markdown.length)
28
+ var i = 0
29
+ while (i < markdown.length) {
30
+ when (markdown[i]) {
31
+ '\r' -> {
32
+ out.append('\n')
33
+ if (i + 1 < markdown.length && markdown[i + 1] == '\n') i++
34
+ }
35
+
36
+ SENTINEL -> {}
37
+
38
+ else -> {
39
+ out.append(markdown[i])
40
+ }
41
+ }
42
+ i++
43
+ }
44
+ return out.toString()
45
+ }
46
+
47
+ /** Prepends one sentinel and replaces every `\n` with one. Length grows by exactly 1. */
48
+ fun encode(canonical: String): String {
49
+ val out = StringBuilder(canonical.length + 1)
50
+ out.append(SENTINEL)
51
+ for (c in canonical) out.append(if (c == '\n') SENTINEL else c)
52
+ return out.toString()
53
+ }
54
+
55
+ /**
56
+ * Reverses [encode] on the parser's plain text. Returns `null` when the leading
57
+ * sentinel is missing — the caller then falls back to the canonical source with no
58
+ * ranges rather than emitting text shifted by an offset that no longer holds.
59
+ */
60
+ fun decode(plainText: String): String? {
61
+ if (plainText.isEmpty() || plainText[0] != SENTINEL) return null
62
+ val out = StringBuilder(plainText.length - 1)
63
+ for (i in 1 until plainText.length) {
64
+ val c = plainText[i]
65
+ out.append(if (c == SENTINEL) '\n' else c)
66
+ }
67
+ return out.toString()
68
+ }
69
+ }
@@ -22,19 +22,73 @@ object InputParser {
22
22
  // A source paragraph break: a newline followed by one or more blank lines.
23
23
  private val PARAGRAPH_BREAK_RE = Regex("\\n[ \\t]*(?:\\n[ \\t]*)+")
24
24
 
25
- fun parseToPlainTextAndRanges(markdown: String): ParseResult {
25
+ fun parseToPlainTextAndRanges(
26
+ markdown: String,
27
+ blockSyntax: Boolean = true,
28
+ ): ParseResult {
26
29
  if (markdown.isEmpty()) {
27
30
  return ParseResult("", emptyList())
28
31
  }
29
32
 
30
- val completed = InputRemend.complete(markdown)
33
+ if (!blockSyntax) {
34
+ return parseWithoutBlockSyntax(markdown)
35
+ }
36
+
37
+ return parseCompleted(InputRemend.complete(markdown), markdown, blockSeparators = true)
38
+ }
39
+
40
+ /**
41
+ * Parses with every block construct hidden behind [BlockSyntaxSentinel], so the whole
42
+ * document lands in one `Paragraph` and list markers, `#`, `>` and friends stay in the
43
+ * text verbatim. `InputRemend` is skipped as well: its speculative closing delimiters
44
+ * are the other source of characters the serializer cannot replay.
45
+ */
46
+ private fun parseWithoutBlockSyntax(markdown: String): ParseResult {
47
+ val canonical = BlockSyntaxSentinel.canonicalize(markdown)
48
+ if (canonical.isEmpty()) {
49
+ return ParseResult("", emptyList())
50
+ }
51
+
52
+ // No block separators: the encoded text has no newline for md4c to drop, so every
53
+ // newline the replay would add is spurious. `$$...$$` is the case that proves it —
54
+ // md4c reports display math as a top-level block even mid-line under the sentinel.
55
+ val parsed = parseCompleted(BlockSyntaxSentinel.encode(canonical), canonical, blockSeparators = false)
56
+ val decoded =
57
+ BlockSyntaxSentinel.decode(parsed.plainText)
58
+ ?: return ParseResult(canonical, emptyList())
59
+
60
+ // Only the leading sentinel is removed, so every offset shifts by exactly one.
61
+ val ranges =
62
+ parsed.formattingRanges.mapNotNull { range ->
63
+ val start = range.start - 1
64
+ val end = range.end - 1
65
+ if (start < 0 || end > decoded.length || end <= start) {
66
+ null
67
+ } else {
68
+ FormattingRange(range.type, start, end, range.url)
69
+ }
70
+ }
71
+ val atoms =
72
+ parsed.mathAtoms.mapNotNull { atom ->
73
+ val position = atom.position - 1
74
+ if (position < 0 || position >= decoded.length) null else atom.copy(position = position)
75
+ }
76
+
77
+ return ParseResult(decoded, ranges, atoms)
78
+ }
79
+
80
+ private fun parseCompleted(
81
+ completed: String,
82
+ fallback: String,
83
+ blockSeparators: Boolean,
84
+ ): ParseResult {
31
85
  val flags =
32
86
  Md4cFlags(
33
87
  underline = true,
34
88
  latexMath = FeatureFlags.IS_MATH_ENABLED,
35
89
  permissiveAutolinks = false,
36
90
  )
37
- val ast = Parser.shared.parseMarkdown(completed, flags) ?: return ParseResult(markdown, emptyList())
91
+ val ast = Parser.shared.parseMarkdown(completed, flags) ?: return ParseResult(fallback, emptyList())
38
92
 
39
93
  val plainText = StringBuilder()
40
94
  val ranges = mutableListOf<FormattingRange>()
@@ -43,14 +97,18 @@ object InputParser {
43
97
  // md4c collapses any blank-line run into a single break, unlike iOS which keeps them.
44
98
  // Re-read the real runs so each break replays its original number of newlines.
45
99
  val blankRuns =
46
- ArrayDeque(
47
- PARAGRAPH_BREAK_RE
48
- .findAll(completed.trim())
49
- .map { match -> match.value.count { it == '\n' } }
50
- .toList(),
51
- )
100
+ if (blockSeparators) {
101
+ ArrayDeque(
102
+ PARAGRAPH_BREAK_RE
103
+ .findAll(completed.trim())
104
+ .map { match -> match.value.count { it == '\n' } }
105
+ .toList(),
106
+ )
107
+ } else {
108
+ ArrayDeque()
109
+ }
52
110
 
53
- walkNode(ast, plainText, ranges, mathAtoms, ArrayDeque(), blankRuns)
111
+ walkNode(ast, plainText, ranges, mathAtoms, ArrayDeque(), blankRuns, blockSeparators)
54
112
 
55
113
  return ParseResult(plainText.toString(), ranges, mathAtoms)
56
114
  }
@@ -62,6 +120,7 @@ object InputParser {
62
120
  mathAtoms: MutableList<MathAtom>,
63
121
  activeStyles: ArrayDeque<ActiveStyle>,
64
122
  blankRuns: ArrayDeque<Int>,
123
+ blockSeparators: Boolean,
65
124
  ) {
66
125
  if (node.type == NodeType.LatexMathInline || node.type == NodeType.LatexMathDisplay) {
67
126
  val latex = extractLatex(node)
@@ -92,10 +151,10 @@ object InputParser {
92
151
 
93
152
  for ((index, child) in node.children.withIndex()) {
94
153
  // Keep the source's blank lines between top-level blocks (md4c drops them, iOS keeps them).
95
- if (index > 0 && child.type.isTopLevelBlock() && plainText.isNotEmpty()) {
154
+ if (blockSeparators && index > 0 && child.type.isTopLevelBlock() && plainText.isNotEmpty()) {
96
155
  plainText.append("\n".repeat(blankRuns.removeFirstOrNull() ?: 2))
97
156
  }
98
- walkNode(child, plainText, ranges, mathAtoms, activeStyles, blankRuns)
157
+ walkNode(child, plainText, ranges, mathAtoms, activeStyles, blankRuns, blockSeparators)
99
158
  }
100
159
 
101
160
  if (styleType != null) {
@@ -8,21 +8,49 @@ class InputLayoutManager(
8
8
  ) {
9
9
  private var forceHeightRecalculationCounter = 0
10
10
 
11
+ /**
12
+ * An invalidation that arrived before `stateWrapper` existed. Fabric applies every prop
13
+ * (and runs `onAfterUpdateTransaction`) inside `ViewManager.updateProperties`, which runs
14
+ * *before* `updateState` assigns the wrapper — so the whole mount transaction invalidates
15
+ * into the void. Without this flag the measurement store stays empty and Yoga keeps the
16
+ * `initialMeasure()` approximation until the next text/prop change.
17
+ */
18
+ private var pending = false
19
+
11
20
  fun invalidateLayout() {
12
- if (view.stateWrapper == null) return
21
+ val stateWrapper = view.stateWrapper
22
+ if (stateWrapper == null) {
23
+ pending = true
24
+ return
25
+ }
26
+ pending = false
13
27
 
14
28
  val text = view.text
15
29
  val paint = view.paint
16
30
 
31
+ // First call for this view measures against cachedWidth = 0 and stores a throwaway size;
32
+ // Yoga immediately re-measures through getMeasureById() with the real width and overwrites
33
+ // it. The state update below is what makes that second pass happen.
17
34
  val needUpdate = InputMeasurementStore.store(view.id, text, paint)
18
35
  if (!needUpdate) return
19
36
 
20
37
  val state = Arguments.createMap()
21
38
  state.putInt("forceHeightRecalculationCounter", forceHeightRecalculationCounter++)
22
- view.stateWrapper?.updateState(state)
39
+ stateWrapper.updateState(state)
40
+ }
41
+
42
+ /**
43
+ * Replays an invalidation that was dropped while `stateWrapper` was null. Called once the
44
+ * wrapper is assigned, i.e. after every prop of the mount transaction has been applied — so
45
+ * the text carries its math chips and the paint carries the resolved typeface/size.
46
+ */
47
+ fun flushPendingInvalidation() {
48
+ if (!pending) return
49
+ invalidateLayout()
23
50
  }
24
51
 
25
52
  fun release() {
53
+ pending = false
26
54
  InputMeasurementStore.release(view.id)
27
55
  }
28
56
  }