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

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 (120) hide show
  1. package/android/build.gradle +5 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +9 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +3 -0
  4. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +50 -0
  8. package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +6 -4
  9. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +38 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +174 -9
  11. package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/BlockSyntaxSentinel.kt +69 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/input/formatting/InputParser.kt +71 -12
  14. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +8 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputLayoutManager.kt +30 -2
  16. package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputMeasurementStore.kt +7 -2
  17. package/android/src/main/java/com/swmansion/enriched/markdown/input/model/MathChipStyle.kt +3 -0
  18. package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +1 -0
  19. package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +8 -1
  20. package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -1
  21. package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +5 -1
  22. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathFontWeight.kt +25 -0
  23. package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +12 -1
  24. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +7 -7
  25. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/MarkdownStyleParser.kt +10 -0
  26. package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
  27. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/extensions/SpannableExtensions.kt +12 -4
  28. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +1 -1
  29. package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +2 -0
  30. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathChipSpan.kt +1 -1
  31. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlinePlaceholderSpan.kt +7 -3
  32. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +8 -5
  33. package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathTypeface.kt +30 -0
  34. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +3 -1
  35. package/docs/API_REFERENCE.md +43 -0
  36. package/docs/INPUT.md +87 -0
  37. package/docs/LATEX_MATH.md +62 -0
  38. package/docs/STYLES.md +3 -0
  39. package/ios/EnrichedMarkdown.mm +44 -12
  40. package/ios/EnrichedMarkdownText.mm +47 -11
  41. package/ios/attachments/ENRMMathInlineAttachment+macOS.m +5 -1
  42. package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
  43. package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
  44. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
  45. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
  46. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  47. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +50 -0
  48. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +29 -0
  49. package/ios/input/ENRMBlockSyntaxSentinel.h +25 -0
  50. package/ios/input/ENRMBlockSyntaxSentinel.mm +80 -0
  51. package/ios/input/ENRMInputFormatter.h +4 -0
  52. package/ios/input/ENRMInputFormatter.mm +2 -0
  53. package/ios/input/ENRMInputParser.h +5 -0
  54. package/ios/input/ENRMInputParser.mm +79 -6
  55. package/ios/input/ENRMInputTextView.mm +11 -1
  56. package/ios/input/EnrichedMarkdownTextInput.h +5 -0
  57. package/ios/input/EnrichedMarkdownTextInput.mm +88 -22
  58. package/ios/input/InputStylePropsUtils.h +13 -0
  59. package/ios/internals/MeasurementCache.h +1 -0
  60. package/ios/math/ENRMRaTeXBridge.swift +52 -3
  61. package/ios/parser/ENRMMarkdownParser.h +8 -0
  62. package/ios/parser/ENRMMarkdownParser.mm +11 -0
  63. package/ios/renderer/ENRMMathInlineRenderer.m +3 -0
  64. package/ios/renderer/ListItemRenderer.m +8 -5
  65. package/ios/styles/StyleConfig.h +6 -0
  66. package/ios/styles/StyleConfig.mm +36 -0
  67. package/ios/utils/ENRMMathFontWeight.h +18 -0
  68. package/ios/utils/StylePropsUtils.h +21 -0
  69. package/ios/views/ENRMMathContainerView.m +3 -1
  70. package/lib/module/EnrichedMarkdownNativeComponent.ts +3 -0
  71. package/lib/module/EnrichedMarkdownTextInput.js +17 -0
  72. package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
  73. package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +34 -0
  74. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +3 -0
  75. package/lib/module/index.js.map +1 -1
  76. package/lib/module/normalizeMarkdownStyle.js +5 -2
  77. package/lib/module/normalizeMarkdownStyle.js.map +1 -1
  78. package/lib/module/normalizeMarkdownStyle.web.js +5 -2
  79. package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
  80. package/lib/module/normalizeMarkdownTextInputStyle.js +7 -3
  81. package/lib/module/normalizeMarkdownTextInputStyle.js.map +1 -1
  82. package/lib/module/web/globalStyles.js +8 -1
  83. package/lib/module/web/globalStyles.js.map +1 -1
  84. package/lib/module/web/styles.js +3 -1
  85. package/lib/module/web/styles.js.map +1 -1
  86. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +3 -0
  87. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  88. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +26 -1
  89. package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
  90. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +28 -0
  91. package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
  92. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +3 -0
  93. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  94. package/lib/typescript/src/index.d.ts +1 -1
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
  97. package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
  98. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts +1 -0
  99. package/lib/typescript/src/normalizeMarkdownTextInputStyle.d.ts.map +1 -1
  100. package/lib/typescript/src/types/MarkdownStyle.d.ts +32 -0
  101. package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
  102. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +3 -0
  103. package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
  104. package/lib/typescript/src/types/events.d.ts +15 -0
  105. package/lib/typescript/src/types/events.d.ts.map +1 -1
  106. package/lib/typescript/src/web/styles.d.ts.map +1 -1
  107. package/package.json +1 -1
  108. package/src/EnrichedMarkdownNativeComponent.ts +3 -0
  109. package/src/EnrichedMarkdownTextInput.tsx +41 -0
  110. package/src/EnrichedMarkdownTextInputNativeComponent.ts +34 -0
  111. package/src/EnrichedMarkdownTextNativeComponent.ts +3 -0
  112. package/src/index.tsx +1 -0
  113. package/src/normalizeMarkdownStyle.ts +6 -1
  114. package/src/normalizeMarkdownStyle.web.ts +6 -1
  115. package/src/normalizeMarkdownTextInputStyle.ts +5 -0
  116. package/src/types/MarkdownStyle.ts +32 -0
  117. package/src/types/MarkdownStyleInternal.ts +3 -0
  118. package/src/types/events.ts +16 -0
  119. package/src/web/globalStyles.ts +7 -0
  120. package/src/web/styles.ts +5 -1
@@ -56,6 +56,7 @@
56
56
  copy.mathPaddingVertical = _mathPaddingVertical;
57
57
  copy.mathSelectedBackgroundColor = _mathSelectedBackgroundColor;
58
58
  copy.mathSelectedBorderColor = _mathSelectedBorderColor;
59
+ copy.mathFontWeight = _mathFontWeight;
59
60
  copy.mathBlockColor = _mathBlockColor;
60
61
  copy.mathBlockFontSize = _mathBlockFontSize;
61
62
  copy.mathBlockBackgroundColor = _mathBlockBackgroundColor;
@@ -69,6 +70,7 @@
69
70
  copy.mathBlockMarginTop = _mathBlockMarginTop;
70
71
  copy.mathBlockMarginBottom = _mathBlockMarginBottom;
71
72
  copy.mathBlockAlignment = _mathBlockAlignment;
73
+ copy.mathBlockFontWeight = _mathBlockFontWeight;
72
74
  return copy;
73
75
  }
74
76
 
@@ -22,6 +22,11 @@ NS_ASSUME_NONNULL_BEGIN
22
22
 
23
23
  - (ENRMParseResult *)parseToPlainTextAndRanges:(NSString *)markdown;
24
24
 
25
+ /// `blockSyntax = NO` hides block structure from md4c (see ENRMBlockSyntaxSentinel)
26
+ /// and skips `ENRMInputRemendComplete`, so list markers, headings and blockquotes
27
+ /// survive as text and the markdown round-trips byte-for-byte.
28
+ - (ENRMParseResult *)parseToPlainTextAndRanges:(NSString *)markdown blockSyntax:(BOOL)blockSyntax;
29
+
25
30
  @end
26
31
 
27
32
  NS_ASSUME_NONNULL_END
@@ -1,4 +1,5 @@
1
1
  #import "ENRMInputParser.h"
2
+ #import "ENRMBlockSyntaxSentinel.h"
2
3
  #import "ENRMFormattingRange.h"
3
4
  #import "ENRMInputRemend.h"
4
5
  #include "md4c.h"
@@ -14,6 +15,11 @@
14
15
  @implementation ENRMParseResult
15
16
  @end
16
17
 
18
+ @interface ENRMInputParser ()
19
+ /// Shared body of both public entry points. `remend` gates `ENRMInputRemendComplete`.
20
+ - (ENRMParseResult *)parseToPlainTextAndRanges:(NSString *)markdown remend:(BOOL)remend;
21
+ @end
22
+
17
23
  @implementation ENRMMathAtom
18
24
  @end
19
25
 
@@ -256,9 +262,9 @@ static int onText(MD_TEXTTYPE textType, const MD_CHAR *text, MD_SIZE size, void
256
262
  return 0;
257
263
  }
258
264
 
259
- static bool runMd4cParse(NSString *markdown, ParseContext &context)
265
+ static bool runMd4cParse(NSString *markdown, ParseContext &context, bool remend)
260
266
  {
261
- NSString *completed = ENRMInputRemendComplete(markdown);
267
+ NSString *completed = remend ? ENRMInputRemendComplete(markdown) : markdown;
262
268
  const char *completedUTF8 = [completed UTF8String];
263
269
  size_t completedLength = strlen(completedUTF8);
264
270
  size_t originalLength = [markdown lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
@@ -286,14 +292,14 @@ struct ParsedSpans {
286
292
  std::vector<MathSpanInfo> mathSpans;
287
293
  };
288
294
 
289
- static NSArray<ENRMInputStyledRange *> *runParseInternal(NSString *markdown, ParsedSpans &out)
295
+ static NSArray<ENRMInputStyledRange *> *runParseInternal(NSString *markdown, ParsedSpans &out, bool remend)
290
296
  {
291
297
  if (markdown.length == 0) {
292
298
  return @[];
293
299
  }
294
300
 
295
301
  ParseContext context;
296
- if (!runMd4cParse(markdown, context)) {
302
+ if (!runMd4cParse(markdown, context, remend)) {
297
303
  return @[];
298
304
  }
299
305
 
@@ -355,10 +361,77 @@ static NSArray<ENRMInputStyledRange *> *runParseInternal(NSString *markdown, Par
355
361
  - (NSArray<ENRMInputStyledRange *> *)parse:(NSString *)markdown
356
362
  {
357
363
  ParsedSpans spans;
358
- return runParseInternal(markdown, spans);
364
+ return runParseInternal(markdown, spans, /* remend */ true);
359
365
  }
360
366
 
361
367
  - (ENRMParseResult *)parseToPlainTextAndRanges:(NSString *)markdown
368
+ {
369
+ return [self parseToPlainTextAndRanges:markdown remend:YES];
370
+ }
371
+
372
+ - (ENRMParseResult *)parseToPlainTextAndRanges:(NSString *)markdown blockSyntax:(BOOL)blockSyntax
373
+ {
374
+ if (blockSyntax) {
375
+ return [self parseToPlainTextAndRanges:markdown];
376
+ }
377
+
378
+ NSString *canonical = ENRMBlockSyntaxCanonicalize(markdown);
379
+ if (canonical.length == 0) {
380
+ ENRMParseResult *empty = [[ENRMParseResult alloc] init];
381
+ empty.plainText = @"";
382
+ empty.formattingRanges = @[];
383
+ empty.mathAtoms = @[];
384
+ return empty;
385
+ }
386
+
387
+ // Remending is skipped too: its speculative closing delimiters are the other
388
+ // source of characters the serializer cannot replay.
389
+ ENRMParseResult *encoded = [self parseToPlainTextAndRanges:ENRMBlockSyntaxEncode(canonical) remend:NO];
390
+
391
+ NSString *decoded = ENRMBlockSyntaxDecode(encoded.plainText);
392
+ ENRMParseResult *result = [[ENRMParseResult alloc] init];
393
+
394
+ if (decoded == nil) {
395
+ result.plainText = canonical;
396
+ result.formattingRanges = @[];
397
+ result.mathAtoms = @[];
398
+ return result;
399
+ }
400
+
401
+ // Only the leading sentinel was removed, so every offset shifts by exactly one.
402
+ NSUInteger decodedLength = decoded.length;
403
+
404
+ NSMutableArray<ENRMFormattingRange *> *shiftedRanges =
405
+ [NSMutableArray arrayWithCapacity:encoded.formattingRanges.count];
406
+ for (ENRMFormattingRange *range in encoded.formattingRanges) {
407
+ NSRange r = range.range;
408
+ if (r.location == 0 || r.length == 0 || NSMaxRange(r) - 1 > decodedLength) {
409
+ continue;
410
+ }
411
+ [shiftedRanges addObject:[ENRMFormattingRange rangeWithType:range.type
412
+ range:NSMakeRange(r.location - 1, r.length)
413
+ url:range.url]];
414
+ }
415
+
416
+ NSMutableArray<ENRMMathAtom *> *shiftedAtoms = [NSMutableArray arrayWithCapacity:encoded.mathAtoms.count];
417
+ for (ENRMMathAtom *atom in encoded.mathAtoms) {
418
+ if (atom.position == 0 || atom.position - 1 >= decodedLength) {
419
+ continue;
420
+ }
421
+ ENRMMathAtom *shifted = [[ENRMMathAtom alloc] init];
422
+ shifted.position = atom.position - 1;
423
+ shifted.latex = atom.latex;
424
+ shifted.displayMode = atom.displayMode;
425
+ [shiftedAtoms addObject:shifted];
426
+ }
427
+
428
+ result.plainText = decoded;
429
+ result.formattingRanges = shiftedRanges;
430
+ result.mathAtoms = shiftedAtoms;
431
+ return result;
432
+ }
433
+
434
+ - (ENRMParseResult *)parseToPlainTextAndRanges:(NSString *)markdown remend:(BOOL)remend
362
435
  {
363
436
  ENRMParseResult *parseResult = [[ENRMParseResult alloc] init];
364
437
 
@@ -370,7 +443,7 @@ static NSArray<ENRMInputStyledRange *> *runParseInternal(NSString *markdown, Par
370
443
  }
371
444
 
372
445
  ParsedSpans parsedSpans;
373
- NSArray<ENRMInputStyledRange *> *styledRanges = runParseInternal(markdown, parsedSpans);
446
+ NSArray<ENRMInputStyledRange *> *styledRanges = runParseInternal(markdown, parsedSpans, remend);
374
447
 
375
448
  // Compute byte map + raw byte ranges for math spans so we can strip the
376
449
  // entire `$...$` (or `$$...$$`) syntax and inject a single `` at each.
@@ -46,12 +46,22 @@ NSString *const kENRMMarkdownPasteboardType = @"com.swmansion.enriched-markdown.
46
46
  markdown = [[NSString alloc] initWithData:markdownValue encoding:NSUTF8StringEncoding];
47
47
  }
48
48
 
49
+ NSString *plainText = pasteboard.string;
50
+
51
+ // Both flavors are read before anything is decided, then nothing is written:
52
+ // JS owns the paste from here and must call resolvePaste. Mutating the buffer
53
+ // first would mean a handler that dies mid-flight leaves the selection gone
54
+ // with no text to show for it — a no-op paste is the visible failure.
55
+ if (self.markdownTextInput.interceptPaste && (plainText.length > 0 || markdown.length > 0)) {
56
+ [self.markdownTextInput emitOnPasteWithText:(plainText ?: @"") markdown:(markdown ?: @"")];
57
+ return;
58
+ }
59
+
49
60
  if (markdown.length > 0 && self.markdownTextInput != nil) {
50
61
  [self.markdownTextInput pasteMarkdown:markdown];
51
62
  return;
52
63
  }
53
64
 
54
- NSString *plainText = pasteboard.string;
55
65
  if (plainText.length > 0) {
56
66
  [self replaceRange:self.selectedTextRange withText:plainText];
57
67
  }
@@ -8,12 +8,17 @@ NS_ASSUME_NONNULL_BEGIN
8
8
 
9
9
  @interface EnrichedMarkdownTextInput : RCTViewComponentView
10
10
  @property (nonatomic, assign) BOOL blockEmitting;
11
+ @property (nonatomic, assign) BOOL interceptPaste;
11
12
  - (CGSize)measureSize:(CGFloat)maxWidth;
12
13
  - (nullable NSString *)markdownForSelectedRange;
13
14
  - (void)copyToClipboard;
14
15
  - (void)pasteMarkdown:(NSString *)markdown;
15
16
  - (void)replaceSelectedTextWith:(NSString *)text formattingRanges:(NSArray *)ranges;
16
17
  - (void)scheduleRelayoutIfNeeded;
18
+ // Paste interception. Declared here rather than in +Internal.h because
19
+ // ENRMInputTextView.mm only imports that header on macOS.
20
+ - (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown;
21
+ - (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown;
17
22
  @end
18
23
 
19
24
  NS_ASSUME_NONNULL_END
@@ -13,6 +13,7 @@
13
13
  #import "ENRMInputTextView.h"
14
14
  #import "ENRMLinkRegexConfig.h"
15
15
  #import "ENRMMarkdownSerializer.h"
16
+ #import "ENRMMathFontWeight.h"
16
17
  #if ENRICHED_MARKDOWN_MATH
17
18
  #import "ENRMMathInlineAttachment.h"
18
19
  #endif
@@ -68,6 +69,8 @@ using namespace facebook::react;
68
69
  BOOL _isApplyingFormatting;
69
70
  BOOL _isTextChanging;
70
71
  BOOL _emitMarkdown;
72
+ BOOL _blockSyntax;
73
+ NSString *_lastImportedMarkdown;
71
74
  CFTimeInterval _lastTextChangeTime;
72
75
 
73
76
  ENRMPlaceholderLabel *_placeholderLabel;
@@ -142,6 +145,8 @@ using namespace facebook::react;
142
145
 
143
146
  self.backgroundColor = [RCTUIColor clearColor];
144
147
  _blockEmitting = NO;
148
+ _blockSyntax = YES;
149
+ _lastImportedMarkdown = nil;
145
150
  _heightUpdateCounter = 0;
146
151
  _formatter = [[ENRMInputFormatter alloc] init];
147
152
  _formatterStyle = [[ENRMInputFormatterStyle alloc] init];
@@ -253,6 +258,20 @@ using namespace facebook::react;
253
258
 
254
259
  #if ENRICHED_MARKDOWN_MATH && !TARGET_OS_OSX
255
260
 
261
+ // Glyph rect of the chip at `index`, in _textView coordinates (the same space
262
+ // `locationInView:_textView` reports). Single source of truth for both the tap
263
+ // hit-test and the rect reported to JS.
264
+ - (CGRect)mathChipRectInTextViewAtIndex:(NSUInteger)index layoutManager:(NSLayoutManager *)lm
265
+ {
266
+ if (lm == nil)
267
+ return CGRectNull;
268
+ NSRange glyphRange = [lm glyphRangeForCharacterRange:NSMakeRange(index, 1) actualCharacterRange:NULL];
269
+ CGRect rect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
270
+ rect.origin.x += _textView.textContainerInset.left;
271
+ rect.origin.y += _textView.textContainerInset.top;
272
+ return rect;
273
+ }
274
+
256
275
  - (void)handleTapForMathChip:(UITapGestureRecognizer *)gesture
257
276
  {
258
277
  if (gesture.state != UIGestureRecognizerStateRecognized)
@@ -265,23 +284,28 @@ using namespace facebook::react;
265
284
  if (idx < 0)
266
285
  return;
267
286
  NSTextStorage *storage = _textView.textStorage;
268
- // The closest position can land just past the chip; probe both `idx` and `idx-1`.
287
+ NSLayoutManager *lm = storage.layoutManagers.firstObject;
288
+ // The closest position can land just past the chip, so probe both `idx` and `idx-1`.
289
+ // `closestPositionToPoint:` has no distance bound — it clamps a tap anywhere in the
290
+ // text view (blank space past the text, textContainerInset padding) onto the nearest
291
+ // character. Requiring the touch to fall inside the chip's own glyph rect is what
292
+ // keeps a tap beside a chip from reading as a tap on it.
269
293
  ENRMMathInlineAttachment *att = nil;
270
294
  NSUInteger chipIdx = NSNotFound;
271
- if ((NSUInteger)idx < storage.length) {
272
- id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)idx effectiveRange:NULL];
273
- if ([v isKindOfClass:[ENRMMathInlineAttachment class]]) {
274
- att = v;
275
- chipIdx = (NSUInteger)idx;
276
- }
277
- }
278
- if (!att && idx > 0) {
279
- NSUInteger probe = (NSUInteger)idx - 1;
280
- id v = [storage attribute:NSAttachmentAttributeName atIndex:probe effectiveRange:NULL];
281
- if ([v isKindOfClass:[ENRMMathInlineAttachment class]]) {
282
- att = v;
283
- chipIdx = probe;
284
- }
295
+ for (NSInteger probe = idx; probe >= idx - 1; probe--) {
296
+ if (probe < 0 || (NSUInteger)probe >= storage.length)
297
+ continue;
298
+ id v = [storage attribute:NSAttachmentAttributeName atIndex:(NSUInteger)probe effectiveRange:NULL];
299
+ if (![v isKindOfClass:[ENRMMathInlineAttachment class]])
300
+ continue;
301
+ // An empty rect means the layout could not give us the chip's extent; accept the
302
+ // candidate then, rather than making the chip untappable.
303
+ CGRect rect = [self mathChipRectInTextViewAtIndex:(NSUInteger)probe layoutManager:lm];
304
+ if (!CGRectIsEmpty(rect) && !CGRectContainsPoint(rect, loc))
305
+ continue;
306
+ att = v;
307
+ chipIdx = (NSUInteger)probe;
308
+ break;
285
309
  }
286
310
  if (!att)
287
311
  return;
@@ -290,7 +314,6 @@ using namespace facebook::react;
290
314
  [self clearMathChipSelectionsExcept:chipRange];
291
315
  att.isSelected = YES;
292
316
  [att invalidateRender];
293
- NSLayoutManager *lm = storage.layoutManagers.firstObject;
294
317
  if (lm) {
295
318
  [lm invalidateLayoutForCharacterRange:chipRange actualCharacterRange:NULL];
296
319
  [lm ensureLayoutForCharacterRange:chipRange];
@@ -306,10 +329,8 @@ using namespace facebook::react;
306
329
  _suppressMathChipClearOnce = YES;
307
330
  _textView.selectedRange = NSMakeRange(caretIndex, 0);
308
331
 
309
- NSRange glyphRange = [lm glyphRangeForCharacterRange:chipRange actualCharacterRange:NULL];
310
- CGRect glyphRect = [lm boundingRectForGlyphRange:glyphRange inTextContainer:_textView.textContainer];
311
- glyphRect.origin.x += _textView.textContainerInset.left;
312
- glyphRect.origin.y += _textView.textContainerInset.top;
332
+ // Recomputed rather than reused from the hit-test: the layout was invalidated above.
333
+ CGRect glyphRect = [self mathChipRectInTextViewAtIndex:chipIdx layoutManager:lm];
313
334
  CGRect rectInSelf = [self.contentView convertRect:glyphRect fromView:_textView];
314
335
 
315
336
  [self emitOnMathPressWithLatex:att.latex start:chipIdx end:chipIdx + 1 displayMode:att.displayMode rect:rectInSelf];
@@ -338,6 +359,35 @@ using namespace facebook::react;
338
359
 
339
360
  #endif
340
361
 
362
+ #pragma mark - Paste
363
+
364
+ - (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown
365
+ {
366
+ auto emitter = [self getEventEmitter];
367
+ if (emitter == nullptr)
368
+ return;
369
+ emitter->onPaste({
370
+ .text = std::string([text UTF8String] ?: ""),
371
+ .markdown = std::string([markdown UTF8String] ?: ""),
372
+ });
373
+ }
374
+
375
+ /// The single exit for an intercepted paste. One command rather than two makes
376
+ /// "never swallow a paste" structural: the branch that turns the clipboard into
377
+ /// something else still has to come through here, with empty strings.
378
+ - (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown
379
+ {
380
+ // Round-trips the formatting when the copy came from an input of ours;
381
+ // without this a copy-then-paste inside the app would land as flat text.
382
+ if (markdown.length > 0) {
383
+ [self pasteMarkdown:markdown];
384
+ return;
385
+ }
386
+ // Empty text with an empty selection is a zero-length edit: it still emits
387
+ // onChangeText with unchanged content, which the caller reads as a no-op.
388
+ [self replaceSelectedTextWith:text formattingRanges:@[]];
389
+ }
390
+
341
391
  #pragma mark - State
342
392
 
343
393
  - (void)updateState:(const facebook::react::State::Shared &)state
@@ -435,6 +485,10 @@ using namespace facebook::react;
435
485
  }
436
486
  }
437
487
 
488
+ if (newViewProps.interceptPaste != oldViewProps.interceptPaste) {
489
+ _interceptPaste = newViewProps.interceptPaste;
490
+ }
491
+
438
492
  if (newViewProps.multiline != oldViewProps.multiline) {
439
493
  _textView.textContainer.maximumNumberOfLines = newViewProps.multiline ? 0 : 1;
440
494
  _textView.textContainer.lineBreakMode =
@@ -546,11 +600,20 @@ using namespace facebook::react;
546
600
  writingDirectionChanged = YES;
547
601
  }
548
602
 
603
+ // Must land before the defaultValue import below, so the first parse already sees
604
+ // the right flag. Assigned unconditionally, not just on change: the generated C++
605
+ // default is `false` while the JS default is `true`, so `blockSyntax={false}` on the
606
+ // first render compares equal to the old props and a change-guard would drop it.
607
+ BOOL blockSyntaxChanged = (_blockSyntax != (BOOL)newViewProps.blockSyntax);
608
+ _blockSyntax = newViewProps.blockSyntax;
609
+
549
610
  if (newViewProps.defaultValue != oldViewProps.defaultValue) {
550
611
  if (!newViewProps.defaultValue.empty() && oldViewProps.defaultValue.empty()) {
551
612
  NSString *markdown = [NSString stringWithUTF8String:newViewProps.defaultValue.c_str()];
552
613
  [self importMarkdown:markdown];
553
614
  }
615
+ } else if (blockSyntaxChanged && _lastImportedMarkdown != nil) {
616
+ [self importMarkdown:_lastImportedMarkdown];
554
617
  }
555
618
 
556
619
  if (styleChanged) {
@@ -694,8 +757,9 @@ using namespace facebook::react;
694
757
 
695
758
  - (void)importMarkdown:(NSString *)markdown
696
759
  {
760
+ _lastImportedMarkdown = [markdown copy];
697
761
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
698
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown];
762
+ ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
699
763
 
700
764
  _blockEmitting = YES;
701
765
 
@@ -754,6 +818,7 @@ using namespace facebook::react;
754
818
  att.blockAlignment = _formatterStyle.mathBlockAlignment;
755
819
  att.selectedBackgroundColor = _formatterStyle.mathBlockSelectedBackgroundColor;
756
820
  att.selectedBorderColor = _formatterStyle.mathBlockSelectedBorderColor;
821
+ att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathBlockFontWeight);
757
822
  } else {
758
823
  att.fontSize = _formatterStyle.mathFontSize > 0 ? _formatterStyle.mathFontSize : 16.0;
759
824
  att.mathTextColor = _formatterStyle.mathColor ?: _formatterStyle.baseTextColor;
@@ -768,6 +833,7 @@ using namespace facebook::react;
768
833
  att.blockAlignment = NSTextAlignmentNatural;
769
834
  att.selectedBackgroundColor = _formatterStyle.mathSelectedBackgroundColor;
770
835
  att.selectedBorderColor = _formatterStyle.mathSelectedBorderColor;
836
+ att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathFontWeight);
771
837
  }
772
838
  }
773
839
 
@@ -854,7 +920,7 @@ using namespace facebook::react;
854
920
  - (void)pasteMarkdown:(NSString *)markdown
855
921
  {
856
922
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
857
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown];
923
+ ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
858
924
  [self replaceSelectedTextWith:parsed.plainText formattingRanges:parsed.formattingRanges];
859
925
  }
860
926
 
@@ -187,6 +187,12 @@ BOOL applyInputStyleProps(ENRMInputFormatterStyle *style, const InputProps &newP
187
187
  style.mathSelectedBorderColor = (sbc && CGColorGetAlpha(sbc.CGColor) > 0) ? sbc : nil;
188
188
  changed = YES;
189
189
  }
190
+ if (newProps.markdownStyle.math.fontWeight != oldProps.markdownStyle.math.fontWeight) {
191
+ if (!newProps.markdownStyle.math.fontWeight.empty()) {
192
+ style.mathFontWeight = [[NSString alloc] initWithUTF8String:newProps.markdownStyle.math.fontWeight.c_str()];
193
+ changed = YES;
194
+ }
195
+ }
190
196
 
191
197
  // Math block (`$$...$$`)
192
198
  if (newProps.markdownStyle.mathBlock.color != oldProps.markdownStyle.mathBlock.color) {
@@ -237,6 +243,13 @@ BOOL applyInputStyleProps(ENRMInputFormatterStyle *style, const InputProps &newP
237
243
  style.mathBlockSelectedBorderColor = (sbc && CGColorGetAlpha(sbc.CGColor) > 0) ? sbc : nil;
238
244
  changed = YES;
239
245
  }
246
+ if (newProps.markdownStyle.mathBlock.fontWeight != oldProps.markdownStyle.mathBlock.fontWeight) {
247
+ if (!newProps.markdownStyle.mathBlock.fontWeight.empty()) {
248
+ style.mathBlockFontWeight =
249
+ [[NSString alloc] initWithUTF8String:newProps.markdownStyle.mathBlock.fontWeight.c_str()];
250
+ changed = YES;
251
+ }
252
+ }
240
253
  if (newProps.markdownStyle.mathBlock.marginTop != oldProps.markdownStyle.mathBlock.marginTop) {
241
254
  style.mathBlockMarginTop = newProps.markdownStyle.mathBlock.marginTop;
242
255
  changed = YES;
@@ -131,6 +131,7 @@ template <typename StyleStruct> inline size_t computeStyleFingerprint(const Styl
131
131
  hashFields(s.table.headerFontFamily, s.table.cellPaddingHorizontal, s.table.cellPaddingVertical, s.table.borderWidth,
132
132
  s.table.borderRadius);
133
133
  hashFields(s.math.fontSize, s.math.padding, s.math.marginTop, s.math.marginBottom, s.math.textAlign);
134
+ hashFields(s.inlineMath.paddingVertical);
134
135
  hashFields(s.taskList.checkboxSize, s.taskList.checkboxBorderRadius);
135
136
 
136
137
  return h;
@@ -10,17 +10,45 @@ import AppKit
10
10
  private typealias UIColor = NSColor
11
11
  #endif
12
12
 
13
+ /// Weight a formula is typeset at.
14
+ ///
15
+ /// An enum rather than the `BOOL` that crosses the ObjC boundary so the draw
16
+ /// path's switch turns a third weight into a compile error. Mirrors
17
+ /// MathFontWeight.kt on Android.
18
+ private enum MathFontWeight {
19
+ case normal
20
+ case bold
21
+
22
+ init(bold: Bool) {
23
+ self = bold ? .bold : .normal
24
+ }
25
+
26
+ /// Stroke added to each glyph outline, as a fraction of the em size. Scales
27
+ /// with the formula so a display block and an inline fraction read alike.
28
+ var strokeEmRatio: CGFloat {
29
+ switch self {
30
+ case .normal: return 0
31
+ case .bold: return 0.035
32
+ }
33
+ }
34
+ }
35
+
13
36
  @objc(ENRMRaTeXRenderResult)
14
37
  public final class ENRMRaTeXRenderResult: NSObject {
15
38
  private let renderer: RaTeXRenderer
39
+ /// Non-zero only in bold mode: half of it grows the glyph ink on each side.
40
+ private let boldStrokeWidth: CGFloat
41
+ private let boldStrokeColor: CGColor?
16
42
 
17
43
  @objc public let width: CGFloat
18
44
  @objc public let ascent: CGFloat
19
45
  @objc public let descent: CGFloat
20
46
  @objc public var totalHeight: CGFloat { ascent + descent }
21
47
 
22
- init(renderer: RaTeXRenderer) {
48
+ init(renderer: RaTeXRenderer, boldStrokeWidth: CGFloat, boldStrokeColor: CGColor?) {
23
49
  self.renderer = renderer
50
+ self.boldStrokeWidth = boldStrokeWidth
51
+ self.boldStrokeColor = boldStrokeColor
24
52
  self.width = renderer.width
25
53
  self.ascent = renderer.height
26
54
  self.descent = renderer.depth
@@ -28,7 +56,22 @@ public final class ENRMRaTeXRenderResult: NSObject {
28
56
  }
29
57
 
30
58
  @objc public func draw(in context: CGContext) {
59
+ guard boldStrokeWidth > 0, let boldStrokeColor else {
60
+ renderer.draw(in: context)
61
+ return
62
+ }
63
+ // Synthetic bold: stroke each glyph outline over its fill, the only way to
64
+ // add weight without moving glyphs whose positions RaTeX has already fixed
65
+ // (the KaTeX bold faces run up to 15% wider and would collide). It reaches
66
+ // every glyph because RaTeXRenderer.drawGlyph sets no text drawing mode of
67
+ // its own, including the paths RaTeX strokes itself (\cancel, \textcircled),
68
+ // which is why bold is opt-in per surface.
69
+ context.saveGState()
70
+ context.setTextDrawingMode(.fillStroke)
71
+ context.setLineWidth(boldStrokeWidth)
72
+ context.setStrokeColor(boldStrokeColor)
31
73
  renderer.draw(in: context)
74
+ context.restoreGState()
32
75
  }
33
76
  }
34
77
 
@@ -43,13 +86,19 @@ public final class ENRMRaTeXBridge: NSObject {
43
86
  _ latex: String,
44
87
  displayMode: Bool,
45
88
  fontSize: CGFloat,
46
- color: UIColor
89
+ color: UIColor,
90
+ bold: Bool
47
91
  ) -> ENRMRaTeXRenderResult? {
48
92
  RaTeXFontLoader.ensureLoaded()
49
93
  do {
50
94
  let displayList = try RaTeXEngine.shared.parse(latex, displayMode: displayMode, color: color)
51
95
  let renderer = RaTeXRenderer(displayList: displayList, fontSize: fontSize)
52
- return ENRMRaTeXRenderResult(renderer: renderer)
96
+ let strokeWidth = fontSize * MathFontWeight(bold: bold).strokeEmRatio
97
+ return ENRMRaTeXRenderResult(
98
+ renderer: renderer,
99
+ boldStrokeWidth: strokeWidth,
100
+ boldStrokeColor: strokeWidth > 0 ? color.cgColor : nil
101
+ )
53
102
  } catch {
54
103
  NSLog("[RaTeX] Failed to parse LaTeX: %@", error.localizedDescription)
55
104
  return nil
@@ -11,6 +11,14 @@
11
11
 
12
12
  + (instancetype)defaultFlags;
13
13
 
14
+ /// Flags matching the codegen `md4cFlags` Props defaults (every flag off).
15
+ ///
16
+ /// Components mirror `md4cFlags` into an ivar with a per-field diff against the
17
+ /// props baseline, so the ivar has to start from the same baseline the props do.
18
+ /// `defaultFlags` enables `latexMath`, which is the right default for the
19
+ /// standalone parser API but would make a `latexMath: false` prop a no-op.
20
+ + (instancetype)propsDefaultFlags;
21
+
14
22
  @end
15
23
 
16
24
  @interface ENRMMarkdownParser : NSObject
@@ -22,6 +22,17 @@ extern MarkdownASTNode *parseMarkdownWithCppParser(NSString *markdown, ENRMMd4cF
22
22
  return [[ENRMMd4cFlags alloc] init];
23
23
  }
24
24
 
25
+ + (instancetype)propsDefaultFlags
26
+ {
27
+ ENRMMd4cFlags *flags = [[ENRMMd4cFlags alloc] init];
28
+ flags.underline = NO;
29
+ flags.latexMath = NO;
30
+ flags.superscript = NO;
31
+ flags.subscript = NO;
32
+ flags.highlight = NO;
33
+ return flags;
34
+ }
35
+
25
36
  - (id)copyWithZone:(NSZone *)zone
26
37
  {
27
38
  ENRMMd4cFlags *copy = [[ENRMMd4cFlags allocWithZone:zone] init];
@@ -1,4 +1,5 @@
1
1
  #import "ENRMMathInlineRenderer.h"
2
+ #import "ENRMMathFontWeight.h"
2
3
  #import "ENRMMathInlineAttachment.h"
3
4
  #import "MarkdownASTNode.h"
4
5
  #import "RenderContext.h"
@@ -25,6 +26,8 @@
25
26
  attachment.latex = latex;
26
27
  attachment.fontSize = currentFont ? currentFont.pointSize : [_config paragraphFontSize];
27
28
  attachment.mathTextColor = [_config inlineMathColor];
29
+ attachment.containerPaddingVertical = [_config inlineMathPaddingVertical];
30
+ attachment.bold = ENRMMathFontWeightIsBold([_config inlineMathFontWeight]);
28
31
 
29
32
  #if TARGET_OS_OSX
30
33
  // On macOS, NSLayoutManager uses self.image/self.bounds rather than calling
@@ -109,15 +109,18 @@ NSString *const TaskIndexAttribute = @"TaskIndex";
109
109
  style.firstLineHeadIndent = totalIndent;
110
110
  style.headIndent = totalIndent;
111
111
 
112
- if (lineHeightConfig > 0) {
113
- style.minimumLineHeight = lineHeightConfig;
114
- style.maximumLineHeight = lineHeightConfig;
115
- }
116
-
117
112
  NSMutableDictionary *attributesToApply = [metadata mutableCopy];
118
113
  attributesToApply[NSParagraphStyleAttributeName] = style;
119
114
 
120
115
  [output addAttributes:attributesToApply range:range];
116
+
117
+ // Line height goes through the shared helper so list items get the same math
118
+ // carve-out as paragraphs. Setting maximumLineHeight here directly would clamp
119
+ // the line fragment below the height an inline math attachment draws at, and the
120
+ // overflow paints over the neighbouring lines. applyLineHeight reads back the
121
+ // paragraph style we just applied, so the indents above survive, and it
122
+ // early-returns on a non-positive line height like the old guard did.
123
+ applyLineHeight(output, range, lineHeightConfig);
121
124
  }];
122
125
 
123
126
  if (isTask && isChecked) {
@@ -378,10 +378,16 @@ NS_ASSUME_NONNULL_BEGIN
378
378
  - (void)setMathMarginBottom:(CGFloat)newValue;
379
379
  - (NSString *)mathTextAlign;
380
380
  - (void)setMathTextAlign:(NSString *)newValue;
381
+ - (NSString *)mathFontWeight;
382
+ - (void)setMathFontWeight:(NSString *)newValue;
381
383
 
382
384
  // Inline Math properties
383
385
  - (RCTUIColor *)inlineMathColor;
384
386
  - (void)setInlineMathColor:(RCTUIColor *)newValue;
387
+ - (CGFloat)inlineMathPaddingVertical;
388
+ - (void)setInlineMathPaddingVertical:(CGFloat)newValue;
389
+ - (NSString *)inlineMathFontWeight;
390
+ - (void)setInlineMathFontWeight:(NSString *)newValue;
385
391
  // Spoiler properties
386
392
  - (RCTUIColor *)spoilerColor;
387
393
  - (void)setSpoilerColor:(RCTUIColor *)newValue;