react-native-enriched-markdown-scaffold 0.7.4-scaffold.7 → 0.7.4-scaffold.9

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 (88) 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 +37 -0
  6. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +8 -0
  7. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +13 -1
  8. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockSyntaxSentinel.kt +69 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +71 -12
  10. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +4 -1
  11. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +3 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +3 -1
  13. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathFontWeight.kt +25 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +12 -1
  15. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +10 -0
  16. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +1 -0
  17. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
  18. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +2 -2
  19. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathTypeface.kt +30 -0
  20. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  21. package/docs/API_REFERENCE.md +9 -0
  22. package/docs/INPUT.md +48 -0
  23. package/docs/LATEX_MATH.md +49 -0
  24. package/docs/STYLES.md +2 -0
  25. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +5 -1
  26. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  27. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  28. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +5 -0
  29. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +37 -0
  30. package/ios/input/ENRMBlockSyntaxSentinel.h +25 -0
  31. package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
  32. package/ios/input/ENRMInputFormatter.h +4 -0
  33. package/ios/input/ENRMInputFormatter.mm +2 -0
  34. package/ios/input/ENRMInputParser.h +5 -0
  35. package/ios/input/ENRMInputParser.mm +79 -6
  36. package/ios/input/EnrichedMarkdownTextInput.mm +19 -2
  37. package/ios/input/InputStylePropsUtils.h +13 -0
  38. package/ios/math/ENRMRaTeXBridge.swift +52 -3
  39. package/ios/renderer/ENRMMathInlineRenderer.m +2 -0
  40. package/ios/styles/StyleConfig.h +4 -0
  41. package/ios/styles/StyleConfig.mm +24 -0
  42. package/ios/utils/ENRMMathFontWeight.h +18 -0
  43. package/ios/utils/StylePropsUtils.h +16 -0
  44. package/ios/views/ENRMMathContainerView.m +3 -1
  45. package/lib/module/EnrichedMarkdownNativeComponent.ts +2 -0
  46. package/lib/module/EnrichedMarkdownTextInput.js +2 -0
  47. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  48. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +12 -0
  49. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +2 -0
  50. package/lib/module/normalizeMarkdownStyle.js +4 -2
  51. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  52. package/lib/module/normalizeMarkdownStyle.web.js +4 -2
  53. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  54. package/lib/module/normalizeMarkdownTextInputStyle.js +7 -3
  55. package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
  56. package/lib/module/web/globalStyles.js +8 -1
  57. package/lib/module/web/globalStyles.js.map +1 -1
  58. package/lib/module/web/styles.js +3 -1
  59. package/lib/module/web/styles.js.map +1 -1
  60. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +2 -0
  61. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  62. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +3 -1
  63. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  64. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +12 -0
  65. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  66. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +2 -0
  67. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  68. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  69. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  70. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +1 -0
  71. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
  72. package/lib/typescript/src/types/MarkdownStyle.d.ts +24 -0
  73. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  74. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +2 -0
  75. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  76. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/EnrichedMarkdownNativeComponent.ts +2 -0
  79. package/src/EnrichedMarkdownTextInput.tsx +4 -0
  80. package/src/EnrichedMarkdownTextInputNativeComponent.ts +12 -0
  81. package/src/EnrichedMarkdownTextNativeComponent.ts +2 -0
  82. package/src/normalizeMarkdownStyle.ts +6 -1
  83. package/src/normalizeMarkdownStyle.web.ts +6 -1
  84. package/src/normalizeMarkdownTextInputStyle.ts +5 -0
  85. package/src/types/MarkdownStyle.ts +24 -0
  86. package/src/types/MarkdownStyleInternal.ts +2 -0
  87. package/src/web/globalStyles.ts +7 -0
  88. 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) {
@@ -1599,6 +1605,7 @@ static inline folly::dynamic toDynamic(const EnrichedMarkdownMarkdownStyleMathSt
1599
1605
  struct EnrichedMarkdownMarkdownStyleInlineMathStruct {
1600
1606
  SharedColor color{};
1601
1607
  Float paddingVertical{0.0};
1608
+ std::string fontWeight{};
1602
1609
 
1603
1610
 
1604
1611
  #ifdef RN_SERIALIZABLE_STATE
@@ -1608,6 +1615,7 @@ struct EnrichedMarkdownMarkdownStyleInlineMathStruct {
1608
1615
  folly::dynamic result = folly::dynamic::object();
1609
1616
  result["color"] = ::facebook::react::toDynamic(color);
1610
1617
  result["paddingVertical"] = paddingVertical;
1618
+ result["fontWeight"] = fontWeight;
1611
1619
  return result;
1612
1620
  }
1613
1621
  #endif
@@ -1624,6 +1632,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
1624
1632
  if (tmp_paddingVertical != map.end()) {
1625
1633
  fromRawValue(context, tmp_paddingVertical->second, result.paddingVertical);
1626
1634
  }
1635
+ auto tmp_fontWeight = map.find("fontWeight");
1636
+ if (tmp_fontWeight != map.end()) {
1637
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
1638
+ }
1627
1639
  }
1628
1640
 
1629
1641
  static inline std::string toString(const EnrichedMarkdownMarkdownStyleInlineMathStruct &value) {
@@ -2810,6 +2822,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathStruct {
2810
2822
  Float paddingVertical{0.0};
2811
2823
  SharedColor selectedBackgroundColor{};
2812
2824
  SharedColor selectedBorderColor{};
2825
+ std::string fontWeight{};
2813
2826
 
2814
2827
 
2815
2828
  #ifdef RN_SERIALIZABLE_STATE
@@ -2827,6 +2840,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathStruct {
2827
2840
  result["paddingVertical"] = paddingVertical;
2828
2841
  result["selectedBackgroundColor"] = ::facebook::react::toDynamic(selectedBackgroundColor);
2829
2842
  result["selectedBorderColor"] = ::facebook::react::toDynamic(selectedBorderColor);
2843
+ result["fontWeight"] = fontWeight;
2830
2844
  return result;
2831
2845
  }
2832
2846
  #endif
@@ -2875,6 +2889,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2875
2889
  if (tmp_selectedBorderColor != map.end()) {
2876
2890
  fromRawValue(context, tmp_selectedBorderColor->second, result.selectedBorderColor);
2877
2891
  }
2892
+ auto tmp_fontWeight = map.find("fontWeight");
2893
+ if (tmp_fontWeight != map.end()) {
2894
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
2895
+ }
2878
2896
  }
2879
2897
 
2880
2898
  static inline std::string toString(const EnrichedMarkdownTextInputMarkdownStyleMathStruct &value) {
@@ -2898,6 +2916,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathBlockStruct {
2898
2916
  Float paddingVertical{0.0};
2899
2917
  SharedColor selectedBackgroundColor{};
2900
2918
  SharedColor selectedBorderColor{};
2919
+ std::string fontWeight{};
2901
2920
  Float marginTop{0.0};
2902
2921
  Float marginBottom{0.0};
2903
2922
  std::string alignment{};
@@ -2918,6 +2937,7 @@ struct EnrichedMarkdownTextInputMarkdownStyleMathBlockStruct {
2918
2937
  result["paddingVertical"] = paddingVertical;
2919
2938
  result["selectedBackgroundColor"] = ::facebook::react::toDynamic(selectedBackgroundColor);
2920
2939
  result["selectedBorderColor"] = ::facebook::react::toDynamic(selectedBorderColor);
2940
+ result["fontWeight"] = fontWeight;
2921
2941
  result["marginTop"] = marginTop;
2922
2942
  result["marginBottom"] = marginBottom;
2923
2943
  result["alignment"] = alignment;
@@ -2969,6 +2989,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
2969
2989
  if (tmp_selectedBorderColor != map.end()) {
2970
2990
  fromRawValue(context, tmp_selectedBorderColor->second, result.selectedBorderColor);
2971
2991
  }
2992
+ auto tmp_fontWeight = map.find("fontWeight");
2993
+ if (tmp_fontWeight != map.end()) {
2994
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
2995
+ }
2972
2996
  auto tmp_marginTop = map.find("marginTop");
2973
2997
  if (tmp_marginTop != map.end()) {
2974
2998
  fromRawValue(context, tmp_marginTop->second, result.marginTop);
@@ -3335,6 +3359,7 @@ class EnrichedMarkdownTextInputProps final : public ViewProps {
3335
3359
  bool editable{false};
3336
3360
  bool autoFocus{false};
3337
3361
  bool scrollEnabled{false};
3362
+ bool blockSyntax{false};
3338
3363
  std::string autoCapitalize{};
3339
3364
  bool multiline{false};
3340
3365
  SharedColor cursorColor{};
@@ -4878,6 +4903,7 @@ struct EnrichedMarkdownTextMarkdownStyleMathStruct {
4878
4903
  Float marginTop{0.0};
4879
4904
  Float marginBottom{0.0};
4880
4905
  std::string textAlign{};
4906
+ std::string fontWeight{};
4881
4907
 
4882
4908
 
4883
4909
  #ifdef RN_SERIALIZABLE_STATE
@@ -4892,6 +4918,7 @@ struct EnrichedMarkdownTextMarkdownStyleMathStruct {
4892
4918
  result["marginTop"] = marginTop;
4893
4919
  result["marginBottom"] = marginBottom;
4894
4920
  result["textAlign"] = textAlign;
4921
+ result["fontWeight"] = fontWeight;
4895
4922
  return result;
4896
4923
  }
4897
4924
  #endif
@@ -4928,6 +4955,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
4928
4955
  if (tmp_textAlign != map.end()) {
4929
4956
  fromRawValue(context, tmp_textAlign->second, result.textAlign);
4930
4957
  }
4958
+ auto tmp_fontWeight = map.find("fontWeight");
4959
+ if (tmp_fontWeight != map.end()) {
4960
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
4961
+ }
4931
4962
  }
4932
4963
 
4933
4964
  static inline std::string toString(const EnrichedMarkdownTextMarkdownStyleMathStruct &value) {
@@ -4943,6 +4974,7 @@ static inline folly::dynamic toDynamic(const EnrichedMarkdownTextMarkdownStyleMa
4943
4974
  struct EnrichedMarkdownTextMarkdownStyleInlineMathStruct {
4944
4975
  SharedColor color{};
4945
4976
  Float paddingVertical{0.0};
4977
+ std::string fontWeight{};
4946
4978
 
4947
4979
 
4948
4980
  #ifdef RN_SERIALIZABLE_STATE
@@ -4952,6 +4984,7 @@ struct EnrichedMarkdownTextMarkdownStyleInlineMathStruct {
4952
4984
  folly::dynamic result = folly::dynamic::object();
4953
4985
  result["color"] = ::facebook::react::toDynamic(color);
4954
4986
  result["paddingVertical"] = paddingVertical;
4987
+ result["fontWeight"] = fontWeight;
4955
4988
  return result;
4956
4989
  }
4957
4990
  #endif
@@ -4968,6 +5001,10 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
4968
5001
  if (tmp_paddingVertical != map.end()) {
4969
5002
  fromRawValue(context, tmp_paddingVertical->second, result.paddingVertical);
4970
5003
  }
5004
+ auto tmp_fontWeight = map.find("fontWeight");
5005
+ if (tmp_fontWeight != map.end()) {
5006
+ fromRawValue(context, tmp_fontWeight->second, result.fontWeight);
5007
+ }
4971
5008
  }
4972
5009
 
4973
5010
  static inline std::string toString(const EnrichedMarkdownTextMarkdownStyleInlineMathStruct &value) {
@@ -170,6 +170,14 @@ class EnrichedMarkdownTextInputManager :
170
170
  view?.isVerticalScrollBarEnabled = value
171
171
  }
172
172
 
173
+ @ReactProp(name = "blockSyntax", defaultBoolean = true)
174
+ override fun setBlockSyntax(
175
+ view: EnrichedMarkdownTextInputView?,
176
+ value: Boolean,
177
+ ) {
178
+ view?.blockSyntax = value
179
+ }
180
+
173
181
  @ReactProp(name = "autoCapitalize")
174
182
  override fun setAutoCapitalize(
175
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 {
@@ -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) {
@@ -99,7 +99,10 @@ object InputMeasurementStore {
99
99
  // invisible characters and over-estimates the height into extra lines, until the next text
100
100
  // change forces a re-measure. Parsing first matches what the editor actually renders.
101
101
  val markdown = props?.getString("defaultValue") ?: props?.getString("placeholder") ?: "I"
102
- val text = InputParser.parseToPlainTextAndRanges(markdown).plainText
102
+ // Must read the same flag the view parses with, or the measured height and the
103
+ // rendered height disagree: `blockSyntax = false` keeps list markers and `#` as text.
104
+ val blockSyntax = if (props?.hasKey("blockSyntax") == true) props.getBoolean("blockSyntax") else true
105
+ val text = InputParser.parseToPlainTextAndRanges(markdown, blockSyntax).plainText
103
106
  val fontSize = props?.getDouble("fontSize")?.toFloat() ?: 16f
104
107
  val spSize = kotlin.math.ceil(PixelUtil.toPixelFromSP(fontSize).toDouble()).toFloat()
105
108
 
@@ -13,6 +13,7 @@ open class MathChipStyle(
13
13
  val paddingVertical: Float,
14
14
  val selectedBackgroundColor: Int?,
15
15
  val selectedBorderColor: Int?,
16
+ val fontWeight: String = "normal",
16
17
  ) {
17
18
  open fun isBlock(): Boolean = false
18
19
  }
@@ -28,6 +29,7 @@ class MathBlockStyle(
28
29
  paddingVertical: Float,
29
30
  selectedBackgroundColor: Int?,
30
31
  selectedBorderColor: Int?,
32
+ fontWeight: String = "normal",
31
33
  val marginTop: Float,
32
34
  val marginBottom: Float,
33
35
  val alignment: Layout.Alignment,
@@ -42,6 +44,7 @@ class MathBlockStyle(
42
44
  paddingVertical,
43
45
  selectedBackgroundColor,
44
46
  selectedBorderColor,
47
+ fontWeight,
45
48
  ) {
46
49
  override fun isBlock(): Boolean = true
47
50
  }
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReadableMap
5
5
  data class InlineMathStyle(
6
6
  val color: Int,
7
7
  val paddingVertical: Float,
8
+ val fontWeight: String,
8
9
  ) {
9
10
  companion object {
10
11
  fun fromReadableMap(
@@ -13,7 +14,8 @@ data class InlineMathStyle(
13
14
  ): InlineMathStyle {
14
15
  val color = parser.parseColor(map, "color")
15
16
  val paddingVertical = parser.toPixelFromDIP(map.getDouble("paddingVertical").toFloat())
16
- return InlineMathStyle(color, paddingVertical)
17
+ val fontWeight = parser.parseString(map, "fontWeight", "normal")
18
+ return InlineMathStyle(color, paddingVertical, fontWeight)
17
19
  }
18
20
  }
19
21
  }
@@ -0,0 +1,25 @@
1
+ package com.swmansion.enriched.markdown.styles
2
+
3
+ /**
4
+ * Weight a formula can actually be typeset at.
5
+ *
6
+ * The style model keeps `fontWeight` as a [String], the way every other
7
+ * `fontWeight` prop in the codebase does, so a value like `"600"` survives as
8
+ * far as the renderer. This enum is the narrowing step at that last hop — see
9
+ * `ratexTypefaceLoader`, the only caller — so adding a third weight is a
10
+ * compile error at the draw site rather than a value that silently falls back
11
+ * to regular. Mirrors ENRMMathFontWeight.h on iOS.
12
+ */
13
+ enum class MathFontWeight {
14
+ NORMAL,
15
+ BOLD,
16
+ ;
17
+
18
+ companion object {
19
+ fun fromString(value: String): MathFontWeight =
20
+ when (value.lowercase()) {
21
+ "bold" -> BOLD
22
+ else -> NORMAL
23
+ }
24
+ }
25
+ }
@@ -10,6 +10,7 @@ data class MathStyle(
10
10
  val marginTop: Float,
11
11
  val marginBottom: Float,
12
12
  val textAlign: String,
13
+ val fontWeight: String,
13
14
  ) {
14
15
  companion object {
15
16
  fun fromReadableMap(
@@ -23,7 +24,17 @@ data class MathStyle(
23
24
  val marginTop = parser.toPixelFromDIP(map.getDouble("marginTop").toFloat())
24
25
  val marginBottom = parser.toPixelFromDIP(map.getDouble("marginBottom").toFloat())
25
26
  val textAlign = parser.parseString(map, "textAlign", "center")
26
- return MathStyle(fontSize, color, backgroundColor, padding, marginTop, marginBottom, textAlign)
27
+ val fontWeight = parser.parseString(map, "fontWeight", "normal")
28
+ return MathStyle(
29
+ fontSize,
30
+ color,
31
+ backgroundColor,
32
+ padding,
33
+ marginTop,
34
+ marginBottom,
35
+ textAlign,
36
+ fontWeight,
37
+ )
27
38
  }
28
39
  }
29
40
  }
@@ -76,6 +76,8 @@ object MarkdownStyleParser {
76
76
  if (mathMap?.hasKey("selectedBackgroundColor") == true) mathMap.getInt("selectedBackgroundColor") else null
77
77
  val selectedBorderColor =
78
78
  if (mathMap?.hasKey("selectedBorderColor") == true) mathMap.getInt("selectedBorderColor") else null
79
+ val fontWeight =
80
+ (if (mathMap?.hasKey("fontWeight") == true) mathMap.getString("fontWeight") else null) ?: "normal"
79
81
 
80
82
  return MathChipStyle(
81
83
  color = color,
@@ -88,6 +90,7 @@ object MarkdownStyleParser {
88
90
  paddingVertical = paddingVertical,
89
91
  selectedBackgroundColor = nullIfTransparent(selectedBackgroundColor),
90
92
  selectedBorderColor = nullIfTransparent(selectedBorderColor),
93
+ fontWeight = fontWeight,
91
94
  )
92
95
  }
93
96
 
@@ -143,6 +146,12 @@ object MarkdownStyleParser {
143
146
  } else {
144
147
  inlineFallback.selectedBorderColor
145
148
  }
149
+ val fontWeight =
150
+ if (mathBlockMap?.hasKey("fontWeight") == true) {
151
+ mathBlockMap.getString("fontWeight") ?: "normal"
152
+ } else {
153
+ inlineFallback.fontWeight
154
+ }
146
155
  val marginTop =
147
156
  PixelUtil.toPixelFromDIP(
148
157
  if (mathBlockMap?.hasKey("marginTop") == true) mathBlockMap.getDouble("marginTop").toFloat() else 8f,
@@ -170,6 +179,7 @@ object MarkdownStyleParser {
170
179
  paddingVertical = paddingVertical,
171
180
  selectedBackgroundColor = nullIfTransparent(selectedBackgroundColor),
172
181
  selectedBorderColor = nullIfTransparent(selectedBorderColor),
182
+ fontWeight = fontWeight,
173
183
  marginTop = marginTop,
174
184
  marginBottom = marginBottom,
175
185
  alignment = alignment,
@@ -36,6 +36,7 @@ class MathInlineRenderer(
36
36
  fontSize = blockStyle.fontSize,
37
37
  textColor = config.style.inlineMathStyle.color,
38
38
  paddingVertical = config.style.inlineMathStyle.paddingVertical,
39
+ fontWeight = config.style.inlineMathStyle.fontWeight,
39
40
  )
40
41
 
41
42
  builder.setSpan(span, start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
@@ -66,7 +66,7 @@ class MathChipSpan(
66
66
  displayMode = displayMode,
67
67
  color = style.color,
68
68
  )
69
- val renderer = RaTeXRenderer(displayList, style.fontSize) { RaTeXFontLoader.getTypeface(it) }
69
+ val renderer = RaTeXRenderer(displayList, style.fontSize, ratexTypefaceLoader(style.fontWeight))
70
70
  formulaWidth = renderer.widthPx
71
71
  formulaAscent = renderer.heightPx
72
72
  formulaDescent = renderer.depthPx
@@ -6,7 +6,6 @@ import android.graphics.Canvas
6
6
  import android.graphics.Paint
7
7
  import android.text.style.ReplacementSpan
8
8
  import io.ratex.RaTeXEngine
9
- import io.ratex.RaTeXFontLoader
10
9
  import io.ratex.RaTeXRenderer
11
10
  import kotlin.math.ceil
12
11
 
@@ -16,6 +15,7 @@ class MathInlineSpan(
16
15
  internal val fontSize: Float,
17
16
  private val textColor: Int,
18
17
  private val paddingVertical: Float,
18
+ private val fontWeight: String = "normal",
19
19
  ) : ReplacementSpan() {
20
20
  private var cachedBitmap: Bitmap? = null
21
21
  private var cachedWidth = 0
@@ -29,7 +29,7 @@ class MathInlineSpan(
29
29
 
30
30
  try {
31
31
  val displayList = RaTeXEngine.parseBlocking(latex, displayMode = false, color = textColor)
32
- val renderer = RaTeXRenderer(displayList, fontSize) { RaTeXFontLoader.getTypeface(it) }
32
+ val renderer = RaTeXRenderer(displayList, fontSize, ratexTypefaceLoader(fontWeight))
33
33
 
34
34
  cachedWidth = renderer.widthPx.toInt().coerceAtLeast(1)
35
35
  mathAscent = renderer.heightPx