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
@@ -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.
@@ -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];
@@ -546,11 +567,20 @@ using namespace facebook::react;
546
567
  writingDirectionChanged = YES;
547
568
  }
548
569
 
570
+ // Must land before the defaultValue import below, so the first parse already sees
571
+ // the right flag. Assigned unconditionally, not just on change: the generated C++
572
+ // default is `false` while the JS default is `true`, so `blockSyntax={false}` on the
573
+ // first render compares equal to the old props and a change-guard would drop it.
574
+ BOOL blockSyntaxChanged = (_blockSyntax != (BOOL)newViewProps.blockSyntax);
575
+ _blockSyntax = newViewProps.blockSyntax;
576
+
549
577
  if (newViewProps.defaultValue != oldViewProps.defaultValue) {
550
578
  if (!newViewProps.defaultValue.empty() && oldViewProps.defaultValue.empty()) {
551
579
  NSString *markdown = [NSString stringWithUTF8String:newViewProps.defaultValue.c_str()];
552
580
  [self importMarkdown:markdown];
553
581
  }
582
+ } else if (blockSyntaxChanged && _lastImportedMarkdown != nil) {
583
+ [self importMarkdown:_lastImportedMarkdown];
554
584
  }
555
585
 
556
586
  if (styleChanged) {
@@ -694,8 +724,9 @@ using namespace facebook::react;
694
724
 
695
725
  - (void)importMarkdown:(NSString *)markdown
696
726
  {
727
+ _lastImportedMarkdown = [markdown copy];
697
728
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
698
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown];
729
+ ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
699
730
 
700
731
  _blockEmitting = YES;
701
732
 
@@ -754,6 +785,7 @@ using namespace facebook::react;
754
785
  att.blockAlignment = _formatterStyle.mathBlockAlignment;
755
786
  att.selectedBackgroundColor = _formatterStyle.mathBlockSelectedBackgroundColor;
756
787
  att.selectedBorderColor = _formatterStyle.mathBlockSelectedBorderColor;
788
+ att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathBlockFontWeight);
757
789
  } else {
758
790
  att.fontSize = _formatterStyle.mathFontSize > 0 ? _formatterStyle.mathFontSize : 16.0;
759
791
  att.mathTextColor = _formatterStyle.mathColor ?: _formatterStyle.baseTextColor;
@@ -768,6 +800,7 @@ using namespace facebook::react;
768
800
  att.blockAlignment = NSTextAlignmentNatural;
769
801
  att.selectedBackgroundColor = _formatterStyle.mathSelectedBackgroundColor;
770
802
  att.selectedBorderColor = _formatterStyle.mathSelectedBorderColor;
803
+ att.bold = ENRMMathFontWeightIsBold(_formatterStyle.mathFontWeight);
771
804
  }
772
805
  }
773
806
 
@@ -854,7 +887,7 @@ using namespace facebook::react;
854
887
  - (void)pasteMarkdown:(NSString *)markdown
855
888
  {
856
889
  ENRMInputParser *parser = [[ENRMInputParser alloc] init];
857
- ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown];
890
+ ENRMParseResult *parsed = [parser parseToPlainTextAndRanges:markdown blockSyntax:_blockSyntax];
858
891
  [self replaceSelectedTextWith:parsed.plainText formattingRanges:parsed.formattingRanges];
859
892
  }
860
893
 
@@ -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;
@@ -224,8 +224,11 @@ static inline NSString *normalizedFontWeight(NSString *fontWeight)
224
224
  CGFloat _mathMarginTop;
225
225
  CGFloat _mathMarginBottom;
226
226
  NSString *_mathTextAlign;
227
+ NSString *_mathFontWeight;
227
228
  // Inline Math properties
228
229
  RCTUIColor *_inlineMathColor;
230
+ CGFloat _inlineMathPaddingVertical;
231
+ NSString *_inlineMathFontWeight;
229
232
  // Spoiler properties
230
233
  RCTUIColor *_spoilerColor;
231
234
  CGFloat _spoilerParticleDensity;
@@ -482,7 +485,10 @@ static inline NSString *normalizedFontWeight(NSString *fontWeight)
482
485
  copy->_mathMarginTop = _mathMarginTop;
483
486
  copy->_mathMarginBottom = _mathMarginBottom;
484
487
  copy->_mathTextAlign = [_mathTextAlign copy];
488
+ copy->_mathFontWeight = [_mathFontWeight copy];
485
489
  copy->_inlineMathColor = [_inlineMathColor copy];
490
+ copy->_inlineMathPaddingVertical = _inlineMathPaddingVertical;
491
+ copy->_inlineMathFontWeight = [_inlineMathFontWeight copy];
486
492
  copy->_spoilerColor = [_spoilerColor copy];
487
493
  copy->_spoilerParticleDensity = _spoilerParticleDensity;
488
494
  copy->_spoilerParticleSpeed = _spoilerParticleSpeed;
@@ -2430,6 +2436,36 @@ static const CGFloat kDefaultMinGap = 4.0;
2430
2436
  _inlineMathColor = newValue;
2431
2437
  }
2432
2438
 
2439
+ - (CGFloat)inlineMathPaddingVertical
2440
+ {
2441
+ return _inlineMathPaddingVertical;
2442
+ }
2443
+
2444
+ - (NSString *)mathFontWeight
2445
+ {
2446
+ return _mathFontWeight;
2447
+ }
2448
+
2449
+ - (void)setMathFontWeight:(NSString *)newValue
2450
+ {
2451
+ _mathFontWeight = newValue;
2452
+ }
2453
+
2454
+ - (NSString *)inlineMathFontWeight
2455
+ {
2456
+ return _inlineMathFontWeight;
2457
+ }
2458
+
2459
+ - (void)setInlineMathFontWeight:(NSString *)newValue
2460
+ {
2461
+ _inlineMathFontWeight = newValue;
2462
+ }
2463
+
2464
+ - (void)setInlineMathPaddingVertical:(CGFloat)newValue
2465
+ {
2466
+ _inlineMathPaddingVertical = newValue;
2467
+ }
2468
+
2433
2469
  // ── Spoiler ─────────────────────────────────────────────────────────────
2434
2470
 
2435
2471
  - (RCTUIColor *)spoilerColor
@@ -0,0 +1,18 @@
1
+ #pragma once
2
+
3
+ #import <Foundation/Foundation.h>
4
+
5
+ /// Weight a formula can actually be typeset at.
6
+ ///
7
+ /// The style config keeps `fontWeight` as an `NSString *`, the way the other 13
8
+ /// `fontWeight` props do, so a value like `@"600"` survives as far as the
9
+ /// renderer. This is the narrowing step at that last hop — the only place on
10
+ /// iOS that decides what "bold" means for math — so the rule lives in one
11
+ /// place instead of at every prop boundary. Mirrors MathFontWeight.kt on
12
+ /// Android.
13
+ ///
14
+ /// `nil` (prop never set) reads as normal, which is the documented default.
15
+ static inline BOOL ENRMMathFontWeightIsBold(NSString *_Nullable fontWeight)
16
+ {
17
+ return [fontWeight isEqualToString:@"bold"];
18
+ }
@@ -1078,6 +1078,14 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty
1078
1078
  changed = YES;
1079
1079
  }
1080
1080
 
1081
+ if (newStyle.math.fontWeight != oldStyle.math.fontWeight) {
1082
+ if (!newStyle.math.fontWeight.empty()) {
1083
+ NSString *fontWeight = [[NSString alloc] initWithUTF8String:newStyle.math.fontWeight.c_str()];
1084
+ [config setMathFontWeight:fontWeight];
1085
+ changed = YES;
1086
+ }
1087
+ }
1088
+
1081
1089
  if (newStyle.math.textAlign != oldStyle.math.textAlign) {
1082
1090
  [config setMathTextAlign:@(newStyle.math.textAlign.c_str())];
1083
1091
  changed = YES;
@@ -1095,6 +1103,19 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty
1095
1103
  changed = YES;
1096
1104
  }
1097
1105
 
1106
+ if (newStyle.inlineMath.fontWeight != oldStyle.inlineMath.fontWeight) {
1107
+ if (!newStyle.inlineMath.fontWeight.empty()) {
1108
+ NSString *fontWeight = [[NSString alloc] initWithUTF8String:newStyle.inlineMath.fontWeight.c_str()];
1109
+ [config setInlineMathFontWeight:fontWeight];
1110
+ changed = YES;
1111
+ }
1112
+ }
1113
+
1114
+ if (newStyle.inlineMath.paddingVertical != oldStyle.inlineMath.paddingVertical) {
1115
+ [config setInlineMathPaddingVertical:newStyle.inlineMath.paddingVertical];
1116
+ changed = YES;
1117
+ }
1118
+
1098
1119
  // ── Spoiler ─────────────────────────────────────────────────────────────
1099
1120
 
1100
1121
  if (newStyle.spoiler.color != oldStyle.spoiler.color) {
@@ -1,6 +1,7 @@
1
1
  #import "ENRMMathContainerView.h"
2
2
  #import "ENRMAccessibilityLabels.h"
3
3
  #import "ENRMFeatureFlags.h"
4
+ #import "ENRMMathFontWeight.h"
4
5
  #include <TargetConditionals.h>
5
6
 
6
7
  #if ENRICHED_MARKDOWN_MATH
@@ -94,7 +95,8 @@
94
95
  ENRMRaTeXRenderResult *result = [ENRMRaTeXBridge parse:latex
95
96
  displayMode:YES
96
97
  fontSize:config.mathFontSize
97
- color:config.mathColor];
98
+ color:config.mathColor
99
+ bold:ENRMMathFontWeightIsBold(config.mathFontWeight)];
98
100
  _mathView.renderResult = result;
99
101
 
100
102
  CGFloat padding = config.mathPadding;
@@ -141,10 +141,13 @@ interface MathStyleInternal {
141
141
  marginTop: CodegenTypes.Float;
142
142
  marginBottom: CodegenTypes.Float;
143
143
  textAlign: string;
144
+ fontWeight: string;
144
145
  }
145
146
 
146
147
  interface InlineMathStyleInternal {
147
148
  color: ColorValue;
149
+ paddingVertical: CodegenTypes.Float;
150
+ fontWeight: string;
148
151
  }
149
152
 
150
153
  interface SpoilerParticlesStyleInternal {