From c9a40b4a9bac9539467556d8c6210f8a0e9cfccf Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Wed, 27 May 2026 16:03:21 +0200 Subject: [PATCH 1/7] feat: integrate RaTeX for LaTeX math rendering, replacing iosMath --- ReactNativeEnrichedMarkdown.podspec | 23 +++- .../project.pbxproj | 38 +++++- .../xcshareddata/swiftpm/Package.resolved | 15 +++ apps/example/ios/Podfile | 6 +- apps/example/ios/Podfile.lock | 116 +++++++++--------- docs/LATEX_MATH.md | 13 +- .../ENRMMathInlineAttachment+macOS.m | 50 +++----- ios/attachments/ENRMMathInlineAttachment.m | 46 +++---- .../ENRMMathInlineAttachmentShared.h | 5 +- ios/math/ENRMRaTeXBridge.swift | 58 +++++++++ ios/utils/ENRMFeatureFlags.h | 4 +- ios/utils/SegmentRenderer.m | 6 +- ios/views/ENRMMathContainerView.m | 115 ++++++++++------- src/types/MarkdownStyle.ts | 2 +- 14 files changed, 311 insertions(+), 186 deletions(-) create mode 100644 apps/example/ios/EnrichedMarkdownExample.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ios/math/ENRMRaTeXBridge.swift diff --git a/ReactNativeEnrichedMarkdown.podspec b/ReactNativeEnrichedMarkdown.podspec index e191da8a..a8cd5470 100644 --- a/ReactNativeEnrichedMarkdown.podspec +++ b/ReactNativeEnrichedMarkdown.podspec @@ -13,22 +13,35 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported, :osx => '14.0' } s.source = { :git => "https://github.com/software-mansion-labs/react-native-enriched-markdown.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm,cpp}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}" - s.private_header_files = "ios/**/*.h" + s.private_header_files = "ios/**/*.h", "cpp/**/*.{h,hpp}" - # To disable LaTeX math rendering (iosMath, supported on iOS and macOS), add ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0' to your Podfile. + # To disable LaTeX math rendering (RaTeX, iOS only), add ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0' to your Podfile. + # When math is enabled, consumers must use `use_frameworks! :linkage => :dynamic` (required for SPM interop). enable_math = ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] != '0' + if enable_math + s.source_files = "ios/**/*.{h,m,mm,cpp,swift}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}" + else + s.source_files = "ios/**/*.{h,m,mm,cpp}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}" + end + preprocessor_defs = '$(inherited) MD4C_USE_UTF8=1' if enable_math preprocessor_defs += ' ENRICHED_MARKDOWN_MATH=1' - s.dependency 'iosMath', '~> 0.9' + if defined?(:spm_dependency) + spm_dependency(s, + url: 'https://github.com/erweixin/RaTeX.git', + requirement: {kind: 'upToNextMajorVersion', minimumVersion: '0.1.9'}, + products: ['RaTeX'] + ) + end end s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp/md4c" "$(PODS_TARGET_SRCROOT)/cpp/parser" "$(PODS_TARGET_SRCROOT)/ios/internals" "$(PODS_TARGET_SRCROOT)/ios/input/internals"', 'GCC_PREPROCESSOR_DEFINITIONS' => preprocessor_defs, - 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17' + 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17', + 'DEFINES_MODULE' => 'YES' } install_modules_dependencies(s) diff --git a/apps/example/ios/EnrichedMarkdownExample.xcodeproj/project.pbxproj b/apps/example/ios/EnrichedMarkdownExample.xcodeproj/project.pbxproj index e96d1a5d..fbbca14d 100644 --- a/apps/example/ios/EnrichedMarkdownExample.xcodeproj/project.pbxproj +++ b/apps/example/ios/EnrichedMarkdownExample.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 029E4BE967864F599BC07069 /* Montserrat-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EFDFCE7DCD1A46B5B744D7EE /* Montserrat-Regular.ttf */; }; - 0C80B921A6F3F58F76C31292 /* libPods-EnrichedMarkdownExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-EnrichedMarkdownExample.a */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 597CBD3D53AB4507A479C53A /* Montserrat-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EFA8D1CE74004DB3BF0ABC53 /* Montserrat-SemiBold.ttf */; }; 62AB1D56FF474B6E8985F1AC /* Montserrat-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B24805265C44464B924D7791 /* Montserrat-Italic.ttf */; }; @@ -18,6 +17,7 @@ 97BABF0A6D86D894F56D1968 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; 9BFD46416F6C43EFBD2EE56C /* Montserrat-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9BC709807BEE4A948F59B9EF /* Montserrat-Medium.ttf */; }; 9E6DECF91246407194021F8B /* CourierPrime-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D495169E422740CF87E4E8EB /* CourierPrime-Regular.ttf */; }; + E7CFEFA7191154F26DE45D8A /* Pods_EnrichedMarkdownExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D4495EFE2AD655EE812CB3F /* Pods_EnrichedMarkdownExample.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -28,7 +28,7 @@ 3B4392A12AC88292D35C810B /* Pods-EnrichedMarkdownExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EnrichedMarkdownExample.debug.xcconfig"; path = "Target Support Files/Pods-EnrichedMarkdownExample/Pods-EnrichedMarkdownExample.debug.xcconfig"; sourceTree = ""; }; 431C95EEA90C4B3995118D3D /* Montserrat-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Montserrat-Bold.ttf"; path = "../assets/fonts/Montserrat-Bold.ttf"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-EnrichedMarkdownExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EnrichedMarkdownExample.release.xcconfig"; path = "Target Support Files/Pods-EnrichedMarkdownExample/Pods-EnrichedMarkdownExample.release.xcconfig"; sourceTree = ""; }; - 5DCACB8F33CDC322A6C60F78 /* libPods-EnrichedMarkdownExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-EnrichedMarkdownExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5D4495EFE2AD655EE812CB3F /* Pods_EnrichedMarkdownExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_EnrichedMarkdownExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = EnrichedMarkdownExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = EnrichedMarkdownExample/LaunchScreen.storyboard; sourceTree = ""; }; 9BC709807BEE4A948F59B9EF /* Montserrat-Medium.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = undefined; includeInIndex = 0; lastKnownFileType = unknown; name = "Montserrat-Medium.ttf"; path = "../assets/fonts/Montserrat-Medium.ttf"; sourceTree = ""; }; @@ -44,7 +44,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 0C80B921A6F3F58F76C31292 /* libPods-EnrichedMarkdownExample.a in Frameworks */, + E7CFEFA7191154F26DE45D8A /* Pods_EnrichedMarkdownExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -67,7 +67,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 5DCACB8F33CDC322A6C60F78 /* libPods-EnrichedMarkdownExample.a */, + 5D4495EFE2AD655EE812CB3F /* Pods_EnrichedMarkdownExample.framework */, ); name = Frameworks; sourceTree = ""; @@ -393,6 +393,21 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/ReactCommon", + "${PODS_ROOT}/ReactCommon/react/nativemodule/core", + "${PODS_ROOT}/React-runtimeexecutor", + "${PODS_ROOT}/React-runtimeexecutor/platform/ios", + "${PODS_ROOT}/ReactCommon-Samples", + "${PODS_ROOT}/ReactCommon-Samples/platform/ios", + "${PODS_ROOT}/React-Fabric/react/renderer/components/view/platform/cxx", + "${PODS_ROOT}/React-NativeModulesApple", + "${PODS_ROOT}/React-graphics", + "${PODS_ROOT}/React-graphics/react/renderer/graphics/platform/ios", + "${PODS_ROOT}/React-featureflags", + "${PODS_ROOT}/React-renderercss", + ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, @@ -468,6 +483,21 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_ROOT}/ReactCommon", + "${PODS_ROOT}/ReactCommon/react/nativemodule/core", + "${PODS_ROOT}/React-runtimeexecutor", + "${PODS_ROOT}/React-runtimeexecutor/platform/ios", + "${PODS_ROOT}/ReactCommon-Samples", + "${PODS_ROOT}/ReactCommon-Samples/platform/ios", + "${PODS_ROOT}/React-Fabric/react/renderer/components/view/platform/cxx", + "${PODS_ROOT}/React-NativeModulesApple", + "${PODS_ROOT}/React-graphics", + "${PODS_ROOT}/React-graphics/react/renderer/graphics/platform/ios", + "${PODS_ROOT}/React-featureflags", + "${PODS_ROOT}/React-renderercss", + ); IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, diff --git a/apps/example/ios/EnrichedMarkdownExample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/example/ios/EnrichedMarkdownExample.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..874a2829 --- /dev/null +++ b/apps/example/ios/EnrichedMarkdownExample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "565035e3bc830e0df269442471c019261284ac60e218a00d2cf8950c1037a837", + "pins" : [ + { + "identity" : "ratex", + "kind" : "remoteSourceControl", + "location" : "https://github.com/erweixin/RaTeX.git", + "state" : { + "revision" : "2d0233e6323f449b95d369264a0090e2ce1ee4cb", + "version" : "0.1.9" + } + } + ], + "version" : 3 +} diff --git a/apps/example/ios/Podfile b/apps/example/ios/Podfile index 68227da3..7c8d2d4b 100644 --- a/apps/example/ios/Podfile +++ b/apps/example/ios/Podfile @@ -1,7 +1,9 @@ ENV['RCT_NEW_ARCH_ENABLED'] = '1' -# Set to '0' to disable LaTeX math rendering and remove the iosMath dependency (~2.5 MB savings). -#ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0' +# LaTeX math rendering (RaTeX). Set to '0' to disable and remove the dependency. +# When enabled, dynamic frameworks are required for SPM interop. +# ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0' +ENV['USE_FRAMEWORKS'] = 'dynamic' if ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] != '0' # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 63423905..56a8c881 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -3,7 +3,6 @@ PODS: - hermes-engine (250829098.0.10): - hermes-engine/Pre-built (= 250829098.0.10) - hermes-engine/Pre-built (250829098.0.10) - - iosMath (0.9.4) - RCTDeprecation (0.85.3) - RCTRequired (0.85.3) - RCTSwiftUI (0.85.3) @@ -1904,7 +1903,6 @@ PODS: - ReactNativeDependencies (0.85.3) - ReactNativeEnrichedMarkdown (0.6.0): - hermes-engine - - iosMath (~> 0.9) - RCTRequired - RCTTypeSafety - React-Core @@ -1977,6 +1975,7 @@ PODS: - React-Core-prebuilt - React-debug - React-Fabric + - React-FabricComponents - React-featureflags - React-graphics - React-ImageManager @@ -2317,10 +2316,6 @@ DEPENDENCIES: - RNWorklets (from `../node_modules/react-native-worklets`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) -SPEC REPOS: - trunk: - - iosMath - EXTERNAL SOURCES: FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" @@ -2494,8 +2489,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d - hermes-engine: 52e1e04154c2786defb1149ebf593d495dae2ae3 - iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba + hermes-engine: 52eaa7366787880ce4ab803bec15b9b969e72d09 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 @@ -2504,81 +2498,81 @@ SPEC CHECKSUMS: React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 - React-Core-prebuilt: 6147d6e420f7eafc5b747002205a958e115878ca + React-Core-prebuilt: 76677a1d1314f90310d9d1968b2d71c229861228 React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 - React-debug: 92944dc4d89f56d640e75498266cbde557a48189 - React-defaultsnativemodule: cd64bc09d7ca24112bbaf1b91edbbcf3d81ea7dc - React-domnativemodule: dacf5bc055ae041039574f38b73a20b91e368774 - React-Fabric: bb0baa33d91839631d315800eb23e9aaa4338a44 - React-FabricComponents: c504d0b0e2f3054b2ba19af839f175cb361153c0 - React-FabricImage: 91eaea1cc58d25ae2596a9277bcfe028f92374c3 - React-featureflags: 5ac0455da0af12ca79b40402e2f42c5c7556b638 - React-featureflagsnativemodule: df7da181b064f10f5959a7cd529b5aab3686ecb2 - React-graphics: d25b1195baf24c7918543f4aff9be89cc080906f + React-debug: 9af1e96a6069c996e3d9f1e493603e74bc9f1593 + React-defaultsnativemodule: ef8c2a55daadc1208d72237c20638a937fc99d1e + React-domnativemodule: 022dc420df8409c2c9b724f872d678f0e73e254c + React-Fabric: feb1cf67568bf2201aa2180f6ed104106edff14f + React-FabricComponents: c6a7ae46e80f1152ada1d13b9e90c8f61689cab7 + React-FabricImage: 6b478ee0986b6c6f8ef925fc1539dbd73ee8c6e1 + React-featureflags: 25925b2a222c4d27fa7625083f9eee3ecc50edf9 + React-featureflagsnativemodule: 0fc8626dc9708a55f8ac0f556b1982ecfa31a6fb + React-graphics: 35f524ebf597dfa00965efeacebab35608b8229b React-hermes: 663286153a8ad6bf752b742654c766ff5e8991e7 - React-idlecallbacksnativemodule: 0bd5392cb67f1ab25df736814b7c05213b1d3c68 - React-ImageManager: a03eed3e3d4222130dc0ad503a1a5f3aa89de746 - React-intersectionobservernativemodule: 5d0f1c3c7b30031b0f6f730ee52dd9d607e2c966 - React-jserrorhandler: d5d6e7e20c5a2d6e8607e18d31d8712d7de676f6 + React-idlecallbacksnativemodule: b02adbab676f22ad603a7db55344b99890db597d + React-ImageManager: bb1606f3c8a8681b98933839d1b3776e74e11cb6 + React-intersectionobservernativemodule: e81bf037d9a4e08efb03a0cccb61361ec5cb9e8d + React-jserrorhandler: 2a5f316dee142d671cec8b9940956120a26225cb React-jsi: eb7cc4cffcf24796cc302d5b2bca0e92544139a9 React-jsiexecutor: 65006f60e64c72c6b82f62ef6bd17c84846e73f8 - React-jsinspector: 01e32e2247b2486117fbb143db7f7717ef462c6d - React-jsinspectorcdp: f1cbb34ca41d188ba22efd9b663cf258a911f6cd - React-jsinspectornetwork: f61acc94c881c41451f508abfe6efa748b956c21 - React-jsinspectortracing: 9395894d9bd4d17931b9afcf230c0e7f4cb3d674 - React-jsitooling: 03ead841daa12a93b18479f5e400ceab3732d36d - React-jsitracing: 4ae61c79e14360d1c6ab566031c62c490da78439 + React-jsinspector: a5cc10871111ac6ae6f60d0030be6546b98c3917 + React-jsinspectorcdp: 9c456fc9218c5ba16531273591cd8e40db38d047 + React-jsinspectornetwork: 8e9ac32459b7af5efa4d5962be734bdc387db090 + React-jsinspectortracing: a36ce197288a347d43aa1dbc09d72dee24269646 + React-jsitooling: 2870fce5bfd23414d39ea141ea6ca4fe67d82b12 + React-jsitracing: d66fb487dd177cf5e1136229de8675da35afac01 React-logger: bf149dea4343a9037b74bade36cced8b63f03f46 - React-Mapbuffer: fec3e025f0ffba6b32cd2a1d7bbdee3e269aae90 - React-microtasksnativemodule: ab33a818d339f5a1da308893c11b487be66121a8 - React-mutationobservernativemodule: a42d1626651ccd7d0dc02a56e69d4ec77c248893 - react-native-safe-area-context: 58eeaba7cb5b8e03bfe91a655d995042f89a74df - react-native-slider: 08ecfc4e9cc16cc388a56dd9caeca51b55cd9bb7 + React-Mapbuffer: 33e3bcefb14c2ce5ba867d908ff16cced1f9b487 + React-microtasksnativemodule: 3a42e75434099fd10030e36fe9f3b27e3811afd5 + React-mutationobservernativemodule: a7f3dc483d3e0e0fade0ea00e2cc95e2e462e023 + react-native-safe-area-context: 690b9dc6cf59e38181d4b7153b5ac372bcd474a6 + react-native-slider: b15258ac3d287a5951f76b11497d659ed20c0def React-NativeModulesApple: deba264b03bd79c6bd61014fa30e40321b5e443a - React-networking: 35e6070b084f435429f85c5db40b4d5b38652fe9 + React-networking: 905170b05255ad7025dd16e9d0cf5a7b9458208a React-oscompat: 64a0c7ef5441855dc6e2a6afe8ba8f92aa05075e React-perflogger: ca04287f205086a1edb5c95882be7b6068458889 - React-performancecdpmetrics: cf1d0a3178ccd59353cedcacbda421f40100a889 - React-performancetimeline: c9771212e7a43032d6f8d5edfb58280d46a7ce1f + React-performancecdpmetrics: bb76b28a76f5384e69174652747f5feb9a4914ac + React-performancetimeline: df497e95ce4439bdef5d5c751c7ca5fbcab47633 React-RCTActionSheet: ab545c1e7b5f1ce4f8b40b6fa06afe2869095884 React-RCTAnimation: 343147a9cd68c93d0ca280799fedfc7102d76ce4 React-RCTAppDelegate: 5054754e92aaa9f8bfabe0f1022b84e46f3dcb57 React-RCTBlob: 8c7ae3422ca4e72bc64b7a0142fd730efc5d4dfb - React-RCTFabric: be458db054b206c4d8e4f20f666e75d5f2c2d420 - React-RCTFBReactNativeSpec: 06db2e8d0f352d9fa23321aed1dd2cde25a3e83c + React-RCTFabric: b6d57f19439c1b02c1dbcf11ee70b9dc6fab8026 + React-RCTFBReactNativeSpec: b57aa2c72a1ea985d49999a8f0fc8f79ad481c9c React-RCTImage: 481457bca63e039eb997f7d16c7560472f49657e React-RCTLinking: 76cbb871240cec2dc5e7aa26c60f59e0ebbcf5a2 React-RCTNetwork: e23a778225b7672e38545d3c5c24e1f4aad6d15f - React-RCTRuntime: 93c830b3ab3f7b494bbe7ae7289784f8b07b3947 + React-RCTRuntime: 43cf561d276a46b8ba217f7c5161547ff35ae43e React-RCTSettings: 96196b535bef147381f96cc60ce9bda85d8be848 React-RCTText: 749ebbd1a999fd84d80f37002ea3bf597fcea6df React-RCTVibration: 5b41a7f274757c2928845981d970916ef9e4ca14 - React-rendererconsistency: 6708acd4bc39c1c5b00164370d0010d93b324c1f - React-renderercss: 80eb778756fed511d6128fc005188ac9008b0baf - React-rendererdebug: b46f338fb9d3f0bea6cf0621016c6c5a7a18e72e - React-RuntimeApple: c494b2089fad4a0c553cf63c2bb265f7eca2285d - React-RuntimeCore: b0bb151c3e2b26c6309d45d05c54aa65a6e0c094 - React-runtimeexecutor: 00b18635b6216a1708f6eb35dbadfd993ec91c7b - React-RuntimeHermes: bb44c4c574ce1b9507cad2e6be015344d18b94a9 - React-runtimescheduler: e1631e57209cb94b3efc29002b6a049cac3f6599 - React-timing: 356b88317ca60d373b0d94b6e7a71b0a572899f5 - React-utils: ccc01da318979af773259c4f6cdb1876f6f86f1a - React-webperformancenativemodule: f8b97c2cb6cfa94e92a503c09ad6d491c50a1390 + React-rendererconsistency: 136a8589d3317a8aed2cbc4a42d8db59d4ca82cc + React-renderercss: 2ca0e84b561309c1d6a80885affa22a669bdfb6a + React-rendererdebug: e994176289d0f50d03e7b8dd512b2ccdba83f79c + React-RuntimeApple: 0a875de8b8210035fd107ca5b91779383a4c0e4e + React-RuntimeCore: cc3e105f674079e2d535bd1508601ff90b3243b2 + React-runtimeexecutor: 2dcc6f4e167a026f7a72268e4b395f5e41c8e641 + React-RuntimeHermes: 6d654deb1405f48a0dd7f5d4b72f9fd6181b4b28 + React-runtimescheduler: bea6c85aa0cc603c5c15789661d0a40b1030df88 + React-timing: 344568a2d138d9beaf1b7815716faf2382472789 + React-utils: 892f56c9e4e0aeadce0391b181af87f419c41758 + React-webperformancenativemodule: 9d79deafb4623b6f7716abdf38294cd6a2f7d4d7 ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 - ReactCodegen: c8f81e6c6f762dcf442a6203a1fb58f7dafc8014 + ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f - ReactNativeDependencies: 75a2f59992523e6ad820ee08850c05a865d2dc63 - ReactNativeEnrichedMarkdown: 861d6937d7f4a6481e8446538dc715c93b545143 - RNCAsyncStorage: d0aafe51ea74a65afce26acb739a4ea975a7a884 - RNDateTimePicker: 7b2738f1e595f83c14580a11c476872755c5befc - RNGestureHandler: 2ff61eac036eaf89f6818bf4ed9c39771a17d134 - RNReanimated: f735b1747a7a93bda7ca102c6d37a3cf54b6d5e8 - RNScreens: 01b065ded2dfe7987bcce770ff3a196be417ff41 - RNSVG: 04044c3abcf177fd674a1a3d13097efa1adebcbe + ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a + ReactNativeEnrichedMarkdown: b99248ae68cab046e6ccd44f190a47e5a003ef3f + RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b + RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 + RNGestureHandler: a97cc64efbfcb7a53969a38310a189a3d5246c65 + RNReanimated: dbe1365727409813696964826b407873304f4d51 + RNScreens: c58f17578c73435d8c00998cac0d89ad8105263c + RNSVG: 2d3af77f13e69994bb8957f69d46d8bcba1a6be8 RNWorklets: 4931990f73bc8f347586918b2e13f11dfadf3b75 - Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b + Yoga: e240fec777ff1f21ef42ccebbb44b6d262125855 -PODFILE CHECKSUM: 9c5417fc84515945aa2357a49779fde55434ae62 +PODFILE CHECKSUM: b36622aa0f914440818675c0f57fee5851155a82 COCOAPODS: 1.16.2 diff --git a/docs/LATEX_MATH.md b/docs/LATEX_MATH.md index 20c7c145..5090c63a 100644 --- a/docs/LATEX_MATH.md +++ b/docs/LATEX_MATH.md @@ -68,7 +68,7 @@ This also prevents KaTeX from being loaded at runtime. ## Disabling LaTeX Math (reducing bundle size) -LaTeX math rendering relies on native third-party libraries — **iosMath** (~2.5 MB) on iOS and **AndroidMath** on Android. These are included by default but can be excluded to reduce your app's binary size. +LaTeX math rendering relies on **RaTeX** — a native, KaTeX-compatible math engine — on both iOS and Android. It is included by default but can be excluded to reduce your app's binary size (~3–5 MB on iOS, varies on Android). ### 1. Disable at the parser level (JS) @@ -88,7 +88,14 @@ Add the following to your Podfile and re-run `pod install`: ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0' ``` -This excludes `iosMath` (~2.5 MB) from the build. Rebuild the app after running `pod install`. +This excludes **RaTeX** from the build. Rebuild the app after running `pod install`. + +> [!NOTE] +> When math is **enabled** (the default), your Podfile must use dynamic frameworks: +> ```ruby +> use_frameworks! :linkage => :dynamic +> ``` +> This is required for CocoaPods to resolve the RaTeX Swift Package dependency. ### 3. Remove the native Android dependency @@ -98,7 +105,7 @@ Add the following to your project's `gradle.properties`: enrichedMarkdown.enableMath=false ``` -This excludes `AndroidMath` from the build. Rebuild the app after changing this property. +This excludes **RaTeX** from the Android build. Rebuild the app after changing this property. ### 4. Expo config plugin diff --git a/ios/attachments/ENRMMathInlineAttachment+macOS.m b/ios/attachments/ENRMMathInlineAttachment+macOS.m index 3639aa33..324a2ac2 100644 --- a/ios/attachments/ENRMMathInlineAttachment+macOS.m +++ b/ios/attachments/ENRMMathInlineAttachment+macOS.m @@ -2,6 +2,12 @@ #if ENRICHED_MARKDOWN_MATH && TARGET_OS_OSX +#if __has_include("ReactNativeEnrichedMarkdown-Swift.h") +#import "ReactNativeEnrichedMarkdown-Swift.h" +#elif __has_include() +#import +#endif + @implementation ENRMMathInlineAttachment (macOS) - (instancetype)init @@ -18,48 +24,30 @@ - (instancetype)init - (void)renderForMacOS { - // MTMathUILabel is an NSView — must be created and laid out on the main thread. - if (![NSThread isMainThread]) { - dispatch_sync(dispatch_get_main_queue(), ^{ [self renderForMacOS]; }); - return; - } - - MTMathUILabel *mathLabel = [[MTMathUILabel alloc] init]; - mathLabel.labelMode = kMTMathUILabelModeText; - mathLabel.textAlignment = kMTTextAlignmentLeft; - mathLabel.fontSize = self.fontSize; - mathLabel.latex = self.latex; - - if (self.mathTextColor) { - mathLabel.textColor = self.mathTextColor; - } - - CGSize labelSize = mathLabel.intrinsicContentSize; - mathLabel.frame = CGRectMake(0, 0, labelSize.width, labelSize.height); - [mathLabel layout]; - - _displayList = mathLabel.displayList; - if (!_displayList) { + RCTUIColor *color = self.mathTextColor ?: [RCTUIColor blackColor]; + ENRMRaTeXRenderResult *result = [ENRMRaTeXBridge parse:self.latex displayMode:NO fontSize:self.fontSize color:color]; + if (!result) return; - } - _mathAscent = _displayList.ascent; - _mathDescent = _displayList.descent; - _cachedSize = CGSizeMake(_displayList.width, _mathAscent + _mathDescent); + _renderResult = result; + _mathAscent = result.ascent; + _mathDescent = result.descent; + _cachedSize = CGSizeMake(ceil(result.width), ceil(result.totalHeight)); // Render the formula into an NSImage. NSLayoutManager draws self.image // automatically when attachmentCell is nil, so this is the reliable // macOS rendering path instead of imageForBounds:textContainer:characterIndex:. // - // NSImage.lockFocus creates a bottom-left origin Quartz context, which matches - // CoreText's coordinate system — no CTM flip is needed here (unlike iOS where - // UIGraphicsImageRenderer uses top-left origin and requires a flip). + // NSImage.lockFocus creates a bottom-left origin Quartz context, but RaTeX + // expects a UIKit-style top-left origin (Y increases downward). Flip the CTM + // so the formula renders right-side up. NSImage *image = [[NSImage alloc] initWithSize:_cachedSize]; [image lockFocus]; CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext]; CGContextSaveGState(ctx); - _displayList.position = CGPointMake(0, _mathDescent); - [_displayList draw:ctx]; + CGContextTranslateCTM(ctx, 0, _cachedSize.height); + CGContextScaleCTM(ctx, 1.0, -1.0); + [_renderResult drawIn:ctx]; CGContextRestoreGState(ctx); [image unlockFocus]; diff --git a/ios/attachments/ENRMMathInlineAttachment.m b/ios/attachments/ENRMMathInlineAttachment.m index b7edabed..4c5c5be8 100644 --- a/ios/attachments/ENRMMathInlineAttachment.m +++ b/ios/attachments/ENRMMathInlineAttachment.m @@ -2,33 +2,30 @@ #if ENRICHED_MARKDOWN_MATH +#if __has_include("ReactNativeEnrichedMarkdown-Swift.h") +#import "ReactNativeEnrichedMarkdown-Swift.h" +#elif __has_include() +#import +#endif + @implementation ENRMMathInlineAttachment #if !TARGET_OS_OSX - (void)prepareIfNeeded { - if (_displayList) + if (_renderResult) return; - MTMathUILabel *mathLabel = [[MTMathUILabel alloc] init]; - mathLabel.labelMode = kMTMathUILabelModeText; - mathLabel.textAlignment = kMTTextAlignmentLeft; - mathLabel.fontSize = self.fontSize; - mathLabel.latex = self.latex; - - if (self.mathTextColor) { - mathLabel.textColor = self.mathTextColor; - } - - [mathLabel layoutIfNeeded]; + RCTUIColor *color = self.mathTextColor ?: [RCTUIColor blackColor]; + ENRMRaTeXRenderResult *result = [ENRMRaTeXBridge parse:self.latex displayMode:NO fontSize:self.fontSize color:color]; + if (!result) + return; - _displayList = mathLabel.displayList; - if (_displayList) { - _mathAscent = _displayList.ascent; - _mathDescent = _displayList.descent; - _cachedSize = CGSizeMake(_displayList.width, _mathAscent + _mathDescent); - } + _renderResult = result; + _mathAscent = result.ascent; + _mathDescent = result.descent; + _cachedSize = CGSizeMake(ceil(result.width), ceil(result.totalHeight)); } - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer @@ -37,7 +34,6 @@ - (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer characterIndex:(NSUInteger)characterIndex { [self prepareIfNeeded]; - return CGRectMake(0, -_mathDescent, _cachedSize.width, _cachedSize.height); } @@ -46,8 +42,7 @@ - (UIImage *)imageForBounds:(CGRect)imageBounds characterIndex:(NSUInteger)characterIndex { [self prepareIfNeeded]; - - if (!_displayList) + if (!_renderResult) return nil; UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat preferredFormat]; @@ -57,15 +52,8 @@ - (UIImage *)imageForBounds:(CGRect)imageBounds return [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { CGContextRef ctx = rendererContext.CGContext; - CGContextSaveGState(ctx); - - CGContextTranslateCTM(ctx, 0, _cachedSize.height); - CGContextScaleCTM(ctx, 1.0, -1.0); - _displayList.position = CGPointMake(0, _mathDescent); - - [_displayList draw:ctx]; - + [_renderResult drawIn:ctx]; CGContextRestoreGState(ctx); }]; } diff --git a/ios/attachments/ENRMMathInlineAttachmentShared.h b/ios/attachments/ENRMMathInlineAttachmentShared.h index a7442e9f..81b3784d 100644 --- a/ios/attachments/ENRMMathInlineAttachmentShared.h +++ b/ios/attachments/ENRMMathInlineAttachmentShared.h @@ -3,13 +3,14 @@ #import "ENRMMathInlineAttachment.h" #if ENRICHED_MARKDOWN_MATH -#import + +@class ENRMRaTeXRenderResult; @interface ENRMMathInlineAttachment () { CGSize _cachedSize; CGFloat _mathAscent; CGFloat _mathDescent; - MTMathListDisplay *_displayList; + ENRMRaTeXRenderResult *_renderResult; } @end diff --git a/ios/math/ENRMRaTeXBridge.swift b/ios/math/ENRMRaTeXBridge.swift new file mode 100644 index 00000000..bc3a9467 --- /dev/null +++ b/ios/math/ENRMRaTeXBridge.swift @@ -0,0 +1,58 @@ +import Foundation +import CoreGraphics +import CoreText +import RaTeX + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +private typealias UIColor = NSColor +#endif + +@objc(ENRMRaTeXRenderResult) +public final class ENRMRaTeXRenderResult: NSObject { + private let renderer: RaTeXRenderer + + @objc public let width: CGFloat + @objc public let ascent: CGFloat + @objc public let descent: CGFloat + @objc public var totalHeight: CGFloat { ascent + descent } + + init(renderer: RaTeXRenderer) { + self.renderer = renderer + self.width = renderer.width + self.ascent = renderer.height + self.descent = renderer.depth + super.init() + } + + @objc public func draw(in context: CGContext) { + renderer.draw(in: context) + } +} + +@objc(ENRMRaTeXBridge) +public final class ENRMRaTeXBridge: NSObject { + + @objc public static func ensureFontsLoaded() { + RaTeXFontLoader.ensureLoaded() + } + + @objc public static func parse( + _ latex: String, + displayMode: Bool, + fontSize: CGFloat, + color: UIColor + ) -> ENRMRaTeXRenderResult? { + RaTeXFontLoader.ensureLoaded() + do { + let displayList = try RaTeXEngine.shared.parse(latex, displayMode: displayMode, color: color) + let renderer = RaTeXRenderer(displayList: displayList, fontSize: fontSize) + return ENRMRaTeXRenderResult(renderer: renderer) + } catch { + NSLog("[RaTeX] Failed to parse LaTeX: %@", error.localizedDescription) + return nil + } + } +} diff --git a/ios/utils/ENRMFeatureFlags.h b/ios/utils/ENRMFeatureFlags.h index fc2c2c9d..f57fa570 100644 --- a/ios/utils/ENRMFeatureFlags.h +++ b/ios/utils/ENRMFeatureFlags.h @@ -1,7 +1,7 @@ #pragma once -// Auto-detect iosMath availability as a fallback for the podspec flag. -#if __has_include() +// Auto-detect RaTeX availability as a fallback for the podspec flag. +#if __has_include() || __has_include("ReactNativeEnrichedMarkdown-Swift.h") #if !defined(ENRICHED_MARKDOWN_MATH) #define ENRICHED_MARKDOWN_MATH 1 #endif diff --git a/ios/utils/SegmentRenderer.m b/ios/utils/SegmentRenderer.m index db31d1cc..f933ff06 100644 --- a/ios/utils/SegmentRenderer.m +++ b/ios/utils/SegmentRenderer.m @@ -28,10 +28,8 @@ NSString *latex = child.children.count > 0 ? child.children.firstObject.content : child.content; [segments addObject:[ENRMMathSegment segmentWithLatex:latex ?: @""]]; #else - // TODO: Fix block math rendering on macOS. Adding ENRMMathContainerView (which - // hosts MTMathUILabel) as a segment causes all preceding text segments to become - // invisible. Likely related to MTMathUILabel.layer.geometryFlipped interacting - // with NSTextView's coordinate system. Inline math ($...$) works. + // TODO: Fix block math rendering on macOS. Adding ENRMMathContainerView as a + // segment causes all preceding text segments to become invisible. #endif } #endif diff --git a/ios/views/ENRMMathContainerView.m b/ios/views/ENRMMathContainerView.m index 761fcc00..55f63561 100644 --- a/ios/views/ENRMMathContainerView.m +++ b/ios/views/ENRMMathContainerView.m @@ -4,7 +4,11 @@ #if ENRICHED_MARKDOWN_MATH #import "PasteboardUtils.h" -#import +#if __has_include("ReactNativeEnrichedMarkdown-Swift.h") +#import "ReactNativeEnrichedMarkdown-Swift.h" +#elif __has_include() +#import +#endif #if TARGET_OS_OSX #import "ENRMMenuAction.h" #endif @@ -12,13 +16,38 @@ #if ENRICHED_MARKDOWN_MATH +@interface ENRMRaTeXCanvasView : RCTUIView +@property (nonatomic, strong, nullable) ENRMRaTeXRenderResult *renderResult; +@end + +@implementation ENRMRaTeXCanvasView + +- (void)drawRect:(CGRect)rect +{ + if (!_renderResult) + return; + CGContextRef ctx = UIGraphicsGetCurrentContext(); + if (!ctx) + return; + [_renderResult drawIn:ctx]; +} + +- (CGSize)intrinsicContentSize +{ + if (!_renderResult) + return CGSizeZero; + return CGSizeMake(ceil(_renderResult.width), ceil(_renderResult.totalHeight)); +} + +@end + #if !TARGET_OS_OSX @interface ENRMMathContainerView () @property (nonatomic, strong, readonly) RCTUIScrollView *scrollView; #else @interface ENRMMathContainerView () #endif -@property (nonatomic, strong, readonly) MTMathUILabel *mathLabel; +@property (nonatomic, strong, readonly) ENRMRaTeXCanvasView *mathView; @property (nonatomic, copy, readwrite) NSString *cachedLatex; @end @@ -31,8 +60,8 @@ - (instancetype)initWithConfig:(StyleConfig *)config _config = config; _cachedLatex = @""; - _mathLabel = [[MTMathUILabel alloc] init]; - _mathLabel.labelMode = kMTMathUILabelModeDisplay; + _mathView = [[ENRMRaTeXCanvasView alloc] initWithFrame:CGRectZero]; + _mathView.backgroundColor = [RCTUIColor clearColor]; #if !TARGET_OS_OSX _scrollView = [[RCTUIScrollView alloc] init]; @@ -42,20 +71,14 @@ - (instancetype)initWithConfig:(StyleConfig *)config _scrollView.alwaysBounceHorizontal = NO; _scrollView.scrollEnabled = NO; [self addSubview:_scrollView]; - [_scrollView addSubview:_mathLabel]; + [_scrollView addSubview:_mathView]; self.isAccessibilityElement = YES; UIContextMenuInteraction *contextMenu = [[UIContextMenuInteraction alloc] initWithDelegate:self]; [self addInteraction:contextMenu]; #else - // MTMathUILabel sets layer.geometryFlipped=YES for CoreText, but React Native - // macOS uses isFlipped=YES views. The combination causes rendering artifacts - // for sibling views. Disable the layer flip — MTMathUILabel's drawRect uses - // CoreText which respects the CGContext transform, and the label's isFlipped=NO - // combined with the parent's isFlipped=YES provides the correct coordinate system. - _mathLabel.layer.geometryFlipped = NO; - [self addSubview:_mathLabel]; + [self addSubview:_mathView]; #endif } return self; @@ -67,20 +90,23 @@ - (void)applyLatex:(NSString *)latex StyleConfig *config = self.config; - _mathLabel.latex = latex; - _mathLabel.fontSize = config.mathFontSize; - _mathLabel.textColor = config.mathColor; - _mathLabel.textAlignment = [self mapAlignment:config.mathTextAlign]; + ENRMRaTeXRenderResult *result = [ENRMRaTeXBridge parse:latex + displayMode:YES + fontSize:config.mathFontSize + color:config.mathColor]; + _mathView.renderResult = result; CGFloat padding = config.mathPadding; -#if !TARGET_OS_OSX - _mathLabel.contentInsets = UIEdgeInsetsMake(padding, padding, padding, padding); -#else - _mathLabel.contentInsets = NSEdgeInsetsMake(padding, padding, padding, padding); -#endif + _mathView.frame = CGRectMake(padding, padding, ceil(result.width), ceil(result.totalHeight)); self.backgroundColor = config.mathBackgroundColor; + [_mathView invalidateIntrinsicContentSize]; +#if !TARGET_OS_OSX + [_mathView setNeedsDisplay]; +#else + [_mathView setNeedsDisplay:YES]; +#endif [self setNeedsLayout]; } @@ -129,45 +155,50 @@ - (void)copyMarkdownToPasteboard copyStringToPasteboard([NSString stringWithFormat:@"$$\n%@\n$$", _cachedLatex]); } -- (MTTextAlignment)mapAlignment:(NSString *)align +- (CGSize)mathViewIntrinsicSize { - if ([align isEqualToString:@"left"]) - return kMTTextAlignmentLeft; - if ([align isEqualToString:@"right"]) - return kMTTextAlignmentRight; - return kMTTextAlignmentCenter; + return _mathView.intrinsicContentSize; } -- (CGSize)mathLabelIntrinsicSize +- (CGFloat)measureHeight:(CGFloat)maxWidth { -#if !TARGET_OS_OSX - return [_mathLabel sizeThatFits:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; -#else - return _mathLabel.intrinsicContentSize; -#endif + CGFloat padding = self.config.mathPadding; + return [self mathViewIntrinsicSize].height + padding * 2; } -- (CGFloat)measureHeight:(CGFloat)maxWidth +- (CGFloat)alignedOriginXForWidth:(CGFloat)formulaWidth inBounds:(CGFloat)boundsWidth padding:(CGFloat)padding { - return [self mathLabelIntrinsicSize].height; + CGFloat available = boundsWidth - padding * 2; + if (formulaWidth >= available) + return padding; + + NSString *align = self.config.mathTextAlign; + if ([align isEqualToString:@"left"]) + return padding; + if ([align isEqualToString:@"right"]) + return padding + available - formulaWidth; + return padding + (available - formulaWidth) / 2.0; } - (void)layoutSubviews { [super layoutSubviews]; - CGSize intrinsicSize = [self mathLabelIntrinsicSize]; - CGFloat contentWidth = MAX(intrinsicSize.width, self.bounds.size.width); + CGFloat padding = self.config.mathPadding; + CGSize intrinsicSize = [self mathViewIntrinsicSize]; + CGFloat contentWidth = intrinsicSize.width + padding * 2; CGFloat contentHeight = self.bounds.size.height; + BOOL overflows = contentWidth > self.bounds.size.width; + CGFloat originX = [self alignedOriginXForWidth:intrinsicSize.width inBounds:self.bounds.size.width padding:padding]; #if !TARGET_OS_OSX _scrollView.frame = self.bounds; - _scrollView.contentSize = CGSizeMake(contentWidth, contentHeight); - _scrollView.scrollEnabled = (intrinsicSize.width > self.bounds.size.width); - _mathLabel.frame = CGRectMake(0, 0, contentWidth, contentHeight); + _scrollView.contentSize = CGSizeMake(overflows ? contentWidth : self.bounds.size.width, contentHeight); + _scrollView.scrollEnabled = overflows; + _mathView.frame = CGRectMake(originX, padding, intrinsicSize.width, intrinsicSize.height); #else - _mathLabel.frame = CGRectMake(0, 0, contentWidth, contentHeight); - [_mathLabel setNeedsDisplay:YES]; + _mathView.frame = CGRectMake(originX, padding, intrinsicSize.width, intrinsicSize.height); + [_mathView setNeedsDisplay:YES]; #endif } diff --git a/src/types/MarkdownStyle.ts b/src/types/MarkdownStyle.ts index a509ec4a..46601632 100644 --- a/src/types/MarkdownStyle.ts +++ b/src/types/MarkdownStyle.ts @@ -298,7 +298,7 @@ export interface Md4cFlags { * Enable LaTeX math span parsing ($..$ and $$..$$). * When enabled, the parser recognizes LaTeX math delimiters. * When disabled, dollar signs are treated as plain text. - * Requires the optional iosMath (iOS) / AndroidMath (Android) native dependencies. + * Requires the optional RaTeX native dependency (iOS and Android). * @default true */ latexMath?: boolean; From 2a5b8e854ff4f3e919c3e03cb4a48e99bee1f84f Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Wed, 27 May 2026 16:40:16 +0200 Subject: [PATCH 2/7] chore: install cocoapods-spm in CI workflow for iOS builds --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e071ade..6b4b2259 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,7 @@ jobs: - name: Install cocoapods if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' run: | + gem install cocoapods-spm cd apps/example/ios pod install env: From 74441492c386aad245c5b8303bd0d7edaa8b16a4 Mon Sep 17 00:00:00 2001 From: Ernest Date: Wed, 3 Jun 2026 13:24:12 +0200 Subject: [PATCH 3/7] fix(ios): resolve RaTeXFFI module redefinition when building with cocoapods-spm cocoapods-spm adds SWIFT_INCLUDE_PATHS on the ReactNativeEnrichedMarkdown target pointing to the build directory, which contains include/module.modulemap defining RaTeXFFI. With use_frameworks! :linkage => :dynamic, RaTeXFFI is also embedded in its XCFramework, so the compiler finds it twice and raises "redefinition of module 'RaTeXFFI'". A before-compile script phase removes the duplicate entry from the shared module map, leaving the XCFramework copy as the authoritative definition. Also adds cocoapods-spm to the example app Gemfile for local bundle exec users. Co-Authored-By: Claude Sonnet 4.6 --- ReactNativeEnrichedMarkdown.podspec | 21 +++++++++++++++++++++ apps/example/Gemfile | 1 + apps/example/ios/Podfile.lock | 8 ++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ReactNativeEnrichedMarkdown.podspec b/ReactNativeEnrichedMarkdown.podspec index a8cd5470..ed3758ec 100644 --- a/ReactNativeEnrichedMarkdown.podspec +++ b/ReactNativeEnrichedMarkdown.podspec @@ -44,5 +44,26 @@ Pod::Spec.new do |s| 'DEFINES_MODULE' => 'YES' } + if enable_math + # cocoapods-spm adds SWIFT_INCLUDE_PATHS on this target pointing to the build dir, + # which contains include/module.modulemap that defines RaTeXFFI. With + # use_frameworks! :linkage => :dynamic, RaTeXFFI is ALSO embedded in its XCFramework, + # so the compiler finds it twice and raises "redefinition of module 'RaTeXFFI'". + # This script runs before compile and removes the duplicate RaTeXFFI entry from the + # shared module map, while the XCFramework copy (accessible via framework search + # paths) remains the authoritative definition. + s.script_phases = [ + { + name: 'Fix RaTeXFFI Module Redefinition', + script: <<~'SCRIPT', + MODULEMAP="${BUILT_PRODUCTS_DIR}/include/module.modulemap" + [ -f "$MODULEMAP" ] || exit 0 + ruby -e 'f=ARGV[0]; c=File.read(f); c2=c.gsub(/\n?(?:framework\s+)?module\s+RaTeXFFI\s*\{[^}]*\}/m,""); File.write(f,c2) if c2!=c' "$MODULEMAP" + SCRIPT + execution_position: :before_compile + } + ] + end + install_modules_dependencies(s) end diff --git a/apps/example/Gemfile b/apps/example/Gemfile index c4cf2ac4..fe1254c3 100644 --- a/apps/example/Gemfile +++ b/apps/example/Gemfile @@ -5,6 +5,7 @@ ruby ">= 2.6.10" # Exclude problematic versions of cocoapods and activesupport that causes build failures. gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' +gem 'cocoapods-spm' gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' gem 'xcodeproj', '< 1.26.0' gem 'concurrent-ruby', '< 1.3.4' diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 56a8c881..9bd9971c 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2489,7 +2489,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d - hermes-engine: 52eaa7366787880ce4ab803bec15b9b969e72d09 + hermes-engine: 7a5738e17537a638701b58a7488ab83a72cddf11 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 @@ -2498,7 +2498,7 @@ SPEC CHECKSUMS: React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 - React-Core-prebuilt: 76677a1d1314f90310d9d1968b2d71c229861228 + React-Core-prebuilt: 300f1f7da5b63a2c480bde8dfc5cde8843905766 React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 React-debug: 9af1e96a6069c996e3d9f1e493603e74bc9f1593 @@ -2562,8 +2562,8 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f - ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a - ReactNativeEnrichedMarkdown: b99248ae68cab046e6ccd44f190a47e5a003ef3f + ReactNativeDependencies: c6c22887dcb9997b770c6751e2a8b7c528e29363 + ReactNativeEnrichedMarkdown: 9c0bc8daf65c4b5a4643708aced4a6c7a14d3785 RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 RNGestureHandler: a97cc64efbfcb7a53969a38310a189a3d5246c65 From 588e3a191a26008cd79eef1b4f679c179bb5e933 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 5 Jun 2026 17:19:53 +0200 Subject: [PATCH 4/7] refactor: remove RaTeXFFI module redefinition script and cocoapods-spm dependency --- .github/workflows/ci.yml | 1 - ReactNativeEnrichedMarkdown.podspec | 21 --------------------- apps/example/Gemfile | 1 - apps/example/ios/Podfile.lock | 10 +++++----- 4 files changed, 5 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 161477c5..beb164e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,7 +168,6 @@ jobs: - name: Install cocoapods if: env.turbo_cache_hit != 1 run: | - gem install cocoapods-spm cd apps/example/ios pod install env: diff --git a/ReactNativeEnrichedMarkdown.podspec b/ReactNativeEnrichedMarkdown.podspec index ed3758ec..a8cd5470 100644 --- a/ReactNativeEnrichedMarkdown.podspec +++ b/ReactNativeEnrichedMarkdown.podspec @@ -44,26 +44,5 @@ Pod::Spec.new do |s| 'DEFINES_MODULE' => 'YES' } - if enable_math - # cocoapods-spm adds SWIFT_INCLUDE_PATHS on this target pointing to the build dir, - # which contains include/module.modulemap that defines RaTeXFFI. With - # use_frameworks! :linkage => :dynamic, RaTeXFFI is ALSO embedded in its XCFramework, - # so the compiler finds it twice and raises "redefinition of module 'RaTeXFFI'". - # This script runs before compile and removes the duplicate RaTeXFFI entry from the - # shared module map, while the XCFramework copy (accessible via framework search - # paths) remains the authoritative definition. - s.script_phases = [ - { - name: 'Fix RaTeXFFI Module Redefinition', - script: <<~'SCRIPT', - MODULEMAP="${BUILT_PRODUCTS_DIR}/include/module.modulemap" - [ -f "$MODULEMAP" ] || exit 0 - ruby -e 'f=ARGV[0]; c=File.read(f); c2=c.gsub(/\n?(?:framework\s+)?module\s+RaTeXFFI\s*\{[^}]*\}/m,""); File.write(f,c2) if c2!=c' "$MODULEMAP" - SCRIPT - execution_position: :before_compile - } - ] - end - install_modules_dependencies(s) end diff --git a/apps/example/Gemfile b/apps/example/Gemfile index fe1254c3..c4cf2ac4 100644 --- a/apps/example/Gemfile +++ b/apps/example/Gemfile @@ -5,7 +5,6 @@ ruby ">= 2.6.10" # Exclude problematic versions of cocoapods and activesupport that causes build failures. gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' -gem 'cocoapods-spm' gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' gem 'xcodeproj', '< 1.26.0' gem 'concurrent-ruby', '< 1.3.4' diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 47aacc66..56a8c881 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2489,7 +2489,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d - hermes-engine: 7a5738e17537a638701b58a7488ab83a72cddf11 + hermes-engine: 52eaa7366787880ce4ab803bec15b9b969e72d09 RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12 RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702 @@ -2498,7 +2498,7 @@ SPEC CHECKSUMS: React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48 React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0 - React-Core-prebuilt: 300f1f7da5b63a2c480bde8dfc5cde8843905766 + React-Core-prebuilt: 76677a1d1314f90310d9d1968b2d71c229861228 React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146 React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716 React-debug: 9af1e96a6069c996e3d9f1e493603e74bc9f1593 @@ -2562,8 +2562,8 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f - ReactNativeDependencies: c6c22887dcb9997b770c6751e2a8b7c528e29363 - ReactNativeEnrichedMarkdown: 9c0bc8daf65c4b5a4643708aced4a6c7a14d3785 + ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a + ReactNativeEnrichedMarkdown: b99248ae68cab046e6ccd44f190a47e5a003ef3f RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 RNGestureHandler: a97cc64efbfcb7a53969a38310a189a3d5246c65 @@ -2575,4 +2575,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b36622aa0f914440818675c0f57fee5851155a82 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 From daf43896d1bdf97d826fce0d87518f43af69d0dc Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 5 Jun 2026 17:42:47 +0200 Subject: [PATCH 5/7] feat: add script phase to fix RaTeXFFI module redefinition for SPM builds --- ReactNativeEnrichedMarkdown.podspec | 17 +++++++++++++++++ apps/example/ios/Podfile.lock | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ReactNativeEnrichedMarkdown.podspec b/ReactNativeEnrichedMarkdown.podspec index a8cd5470..9cd80afd 100644 --- a/ReactNativeEnrichedMarkdown.podspec +++ b/ReactNativeEnrichedMarkdown.podspec @@ -44,5 +44,22 @@ Pod::Spec.new do |s| 'DEFINES_MODULE' => 'YES' } + if enable_math + # Xcode generates a module.modulemap at ${BUILT_PRODUCTS_DIR}/include/ for SPM + # packages that re-declares RaTeXFFI — but the RaTeX XCFramework already ships + # its own definition. Strip the duplicate before compile. + s.script_phases = [ + { + name: 'Fix RaTeXFFI Module Redefinition', + script: <<~'SCRIPT', + MODULEMAP="${BUILT_PRODUCTS_DIR}/include/module.modulemap" + [ -f "$MODULEMAP" ] || exit 0 + sed -i '' -E '/^(framework )?module RaTeXFFI /,/^\}/d' "$MODULEMAP" + SCRIPT + execution_position: :before_compile + } + ] + end + install_modules_dependencies(s) end diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 56a8c881..ce2baba6 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2563,7 +2563,7 @@ SPEC CHECKSUMS: ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a - ReactNativeEnrichedMarkdown: b99248ae68cab046e6ccd44f190a47e5a003ef3f + ReactNativeEnrichedMarkdown: 684419037bf47f3d318f61fb0e63e277e1b398ac RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 RNGestureHandler: a97cc64efbfcb7a53969a38310a189a3d5246c65 From e0e471da76afaebe5ed793b8bfc741b2bbfab0b4 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 5 Jun 2026 19:50:33 +0200 Subject: [PATCH 6/7] fix: update ReactNativeEnrichedMarkdown podspec to exclude math files when math is disabled --- ReactNativeEnrichedMarkdown.podspec | 23 +++++++++++++++-------- apps/example/Gemfile.lock | 7 ++----- apps/example/ios/Podfile.lock | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/ReactNativeEnrichedMarkdown.podspec b/ReactNativeEnrichedMarkdown.podspec index 9cd80afd..93f4ea8a 100644 --- a/ReactNativeEnrichedMarkdown.podspec +++ b/ReactNativeEnrichedMarkdown.podspec @@ -19,10 +19,9 @@ Pod::Spec.new do |s| # When math is enabled, consumers must use `use_frameworks! :linkage => :dynamic` (required for SPM interop). enable_math = ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] != '0' - if enable_math - s.source_files = "ios/**/*.{h,m,mm,cpp,swift}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}" - else - s.source_files = "ios/**/*.{h,m,mm,cpp}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}" + s.source_files = "ios/**/*.{h,m,mm,cpp,swift}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}" + unless enable_math + s.exclude_files = "ios/math/**/*.swift" end preprocessor_defs = '$(inherited) MD4C_USE_UTF8=1' @@ -45,16 +44,24 @@ Pod::Spec.new do |s| } if enable_math - # Xcode generates a module.modulemap at ${BUILT_PRODUCTS_DIR}/include/ for SPM - # packages that re-declares RaTeXFFI — but the RaTeX XCFramework already ships - # its own definition. Strip the duplicate before compile. + # cocoapods-spm generates a module.modulemap at ${BUILT_PRODUCTS_DIR}/include/ + # that defines RaTeXFFI. The RaTeX XCFramework ships its own definition too. + # When both are visible the compiler raises "redefinition of module 'RaTeXFFI'". + # Only strip the shared entry when a second definition actually exists; otherwise + # local Xcode builds that rely on it as the sole source would break. s.script_phases = [ { name: 'Fix RaTeXFFI Module Redefinition', script: <<~'SCRIPT', MODULEMAP="${BUILT_PRODUCTS_DIR}/include/module.modulemap" [ -f "$MODULEMAP" ] || exit 0 - sed -i '' -E '/^(framework )?module RaTeXFFI /,/^\}/d' "$MODULEMAP" + grep -q 'module RaTeXFFI' "$MODULEMAP" || exit 0 + + MAPS=$(find "${BUILT_PRODUCTS_DIR}" -name "module.modulemap" \ + -exec grep -l 'module RaTeXFFI' {} + 2>/dev/null | wc -l) + if [ "$MAPS" -gt 1 ]; then + sed -i '' -E '/^(framework )?module RaTeXFFI /,/^\}/d' "$MODULEMAP" + fi SCRIPT execution_position: :before_compile } diff --git a/apps/example/Gemfile.lock b/apps/example/Gemfile.lock index ba4222a8..5fdefe22 100644 --- a/apps/example/Gemfile.lock +++ b/apps/example/Gemfile.lock @@ -63,7 +63,7 @@ GEM cocoapods-try (1.2.0) colored2 (3.1.2) concurrent-ruby (1.3.3) - connection_pool (3.0.2) + connection_pool (2.5.5) drb (2.2.3) escape (0.0.4) ethon (0.15.0) @@ -78,16 +78,13 @@ GEM concurrent-ruby (~> 1.0) json (2.18.1) logger (1.7.0) - minitest (6.0.2) - drb (~> 2.0) - prism (~> 1.5) + minitest (5.27.0) molinillo (0.8.0) mutex_m (0.3.0) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) nkf (0.1.3) - prism (1.9.0) public_suffix (4.0.7) rexml (3.4.4) ruby-macho (2.5.1) diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index ce2baba6..dd2ffb2b 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2563,7 +2563,7 @@ SPEC CHECKSUMS: ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a - ReactNativeEnrichedMarkdown: 684419037bf47f3d318f61fb0e63e277e1b398ac + ReactNativeEnrichedMarkdown: 78717ccccb7362dcc843ea50ae09342b2198dd38 RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 RNGestureHandler: a97cc64efbfcb7a53969a38310a189a3d5246c65 From f6a289722f0b2bb24ff5272a660fcb9ed5ba12fc Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 5 Jun 2026 20:34:22 +0200 Subject: [PATCH 7/7] fix: update RaTeXFFI module redefinition script for correct module path in podspec --- ReactNativeEnrichedMarkdown.podspec | 21 ++++++++------------- apps/example/ios/Podfile.lock | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/ReactNativeEnrichedMarkdown.podspec b/ReactNativeEnrichedMarkdown.podspec index 93f4ea8a..d7030c72 100644 --- a/ReactNativeEnrichedMarkdown.podspec +++ b/ReactNativeEnrichedMarkdown.podspec @@ -44,24 +44,19 @@ Pod::Spec.new do |s| } if enable_math - # cocoapods-spm generates a module.modulemap at ${BUILT_PRODUCTS_DIR}/include/ - # that defines RaTeXFFI. The RaTeX XCFramework ships its own definition too. - # When both are visible the compiler raises "redefinition of module 'RaTeXFFI'". - # Only strip the shared entry when a second definition actually exists; otherwise - # local Xcode builds that rely on it as the sole source would break. + # React Native's spm_dependency generates a module.modulemap at + # ${BUILT_PRODUCTS_DIR}/include/ that re-declares RaTeXFFI, but the RaTeX + # XCFramework already ships its own definition. Strip the duplicate to + # prevent "redefinition of module 'RaTeXFFI'" during compilation. s.script_phases = [ { name: 'Fix RaTeXFFI Module Redefinition', script: <<~'SCRIPT', - MODULEMAP="${BUILT_PRODUCTS_DIR}/include/module.modulemap" + # The shared module.modulemap lives in the platform build-products dir, + # one level above the per-target BUILT_PRODUCTS_DIR that CocoaPods sets. + MODULEMAP="${BUILT_PRODUCTS_DIR}/../include/module.modulemap" [ -f "$MODULEMAP" ] || exit 0 - grep -q 'module RaTeXFFI' "$MODULEMAP" || exit 0 - - MAPS=$(find "${BUILT_PRODUCTS_DIR}" -name "module.modulemap" \ - -exec grep -l 'module RaTeXFFI' {} + 2>/dev/null | wc -l) - if [ "$MAPS" -gt 1 ]; then - sed -i '' -E '/^(framework )?module RaTeXFFI /,/^\}/d' "$MODULEMAP" - fi + sed -i '' -E '/^(framework )?module RaTeXFFI /,/^\}/d' "$MODULEMAP" SCRIPT execution_position: :before_compile } diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index dd2ffb2b..865be080 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2563,7 +2563,7 @@ SPEC CHECKSUMS: ReactCodegen: 7016a2114079361a2f1536b3c91a15ceb8eb7ca4 ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f ReactNativeDependencies: b8ec997507405a87f23927c21c3693d5859efb9a - ReactNativeEnrichedMarkdown: 78717ccccb7362dcc843ea50ae09342b2198dd38 + ReactNativeEnrichedMarkdown: 255466bca4c9778a2d4f7c6cfb974c7e8a277ee2 RNCAsyncStorage: 7d913885caaae13f5fb62dd0bd04c674ea8bf97b RNDateTimePicker: afe0288e6c7855994f7f2617b7b2c05799fea7f3 RNGestureHandler: a97cc64efbfcb7a53969a38310a189a3d5246c65