@fendent/react-native-enriched 0.5.2-fork.1

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 (278) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +343 -0
  3. package/ReactNativeEnriched.podspec +31 -0
  4. package/android/build.gradle +106 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +197 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +72 -0
  7. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
  10. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
  11. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.cpp +173 -0
  12. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.h +833 -0
  13. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
  15. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/lint.gradle +70 -0
  19. package/android/src/main/AndroidManifest.xml +3 -0
  20. package/android/src/main/AndroidManifestNew.xml +2 -0
  21. package/android/src/main/java/com/swmansion/enriched/ReactNativeEnrichedPackage.kt +20 -0
  22. package/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt +126 -0
  23. package/android/src/main/java/com/swmansion/enriched/common/CheckboxDrawable.kt +81 -0
  24. package/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt +11 -0
  25. package/android/src/main/java/com/swmansion/enriched/common/EnrichedStyle.kt +57 -0
  26. package/android/src/main/java/com/swmansion/enriched/common/ForceRedrawSpan.kt +14 -0
  27. package/android/src/main/java/com/swmansion/enriched/common/GumboNormalizer.kt +5 -0
  28. package/android/src/main/java/com/swmansion/enriched/common/MentionStyle.kt +7 -0
  29. package/android/src/main/java/com/swmansion/enriched/common/ResourceManager.kt +26 -0
  30. package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +956 -0
  31. package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +79 -0
  32. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBlockQuoteSpan.kt +53 -0
  33. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBoldSpan.kt +12 -0
  34. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt +92 -0
  35. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCodeBlockSpan.kt +81 -0
  36. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH1Span.kt +20 -0
  37. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH2Span.kt +20 -0
  38. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH3Span.kt +20 -0
  39. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH4Span.kt +21 -0
  40. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH5Span.kt +20 -0
  41. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH6Span.kt +20 -0
  42. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt +184 -0
  43. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedInlineCodeSpan.kt +24 -0
  44. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedItalicSpan.kt +12 -0
  45. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedLinkSpan.kt +29 -0
  46. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedMentionSpan.kt +35 -0
  47. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedOrderedListSpan.kt +79 -0
  48. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedStrikeThroughSpan.kt +11 -0
  49. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnderlineSpan.kt +11 -0
  50. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt +62 -0
  51. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedBlockSpan.kt +5 -0
  52. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedHeadingSpan.kt +3 -0
  53. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedInlineSpan.kt +3 -0
  54. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedParagraphSpan.kt +5 -0
  55. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedSpan.kt +3 -0
  56. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +4 -0
  57. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputConnectionWrapper.kt +140 -0
  58. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +83 -0
  59. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +1120 -0
  60. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewLayoutManager.kt +27 -0
  61. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +478 -0
  62. package/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt +225 -0
  63. package/android/src/main/java/com/swmansion/enriched/textinput/events/MentionHandler.kt +55 -0
  64. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeHtmlEvent.kt +27 -0
  65. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeSelectionEvent.kt +30 -0
  66. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeStateEvent.kt +21 -0
  67. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeTextEvent.kt +30 -0
  68. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt +35 -0
  69. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputBlurEvent.kt +25 -0
  70. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputFocusEvent.kt +25 -0
  71. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputKeyPressEvent.kt +27 -0
  72. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnLinkDetectedEvent.kt +32 -0
  73. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionDetectedEvent.kt +30 -0
  74. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionEvent.kt +34 -0
  75. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnPasteImagesEvent.kt +47 -0
  76. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnRequestHtmlResultEvent.kt +32 -0
  77. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnSubmitEditingEvent.kt +29 -0
  78. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBlockQuoteSpan.kt +14 -0
  79. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBoldSpan.kt +14 -0
  80. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCheckboxListSpan.kt +15 -0
  81. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCodeBlockSpan.kt +14 -0
  82. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH1Span.kt +14 -0
  83. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH2Span.kt +14 -0
  84. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH3Span.kt +14 -0
  85. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH4Span.kt +14 -0
  86. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH5Span.kt +14 -0
  87. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH6Span.kt +14 -0
  88. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt +36 -0
  89. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputInlineCodeSpan.kt +14 -0
  90. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputItalicSpan.kt +14 -0
  91. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputLinkSpan.kt +16 -0
  92. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputMentionSpan.kt +18 -0
  93. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputOrderedListSpan.kt +21 -0
  94. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputStrikeThroughSpan.kt +14 -0
  95. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnderlineSpan.kt +14 -0
  96. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnorderedListSpan.kt +14 -0
  97. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt +44 -0
  98. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedSpans.kt +241 -0
  99. package/android/src/main/java/com/swmansion/enriched/textinput/spans/interfaces/EnrichedInputSpan.kt +10 -0
  100. package/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt +372 -0
  101. package/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +164 -0
  102. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +263 -0
  103. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +434 -0
  104. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +394 -0
  105. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedEditableFactory.kt +17 -0
  106. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +320 -0
  107. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpanState.kt +310 -0
  108. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +106 -0
  109. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannableStringBuilder.kt +24 -0
  110. package/android/src/main/java/com/swmansion/enriched/textinput/utils/RichContentReceiver.kt +127 -0
  111. package/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +106 -0
  112. package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedSpanWatcher.kt +107 -0
  113. package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt +74 -0
  114. package/android/src/main/new_arch/CMakeLists.txt +62 -0
  115. package/android/src/main/new_arch/GumboNormalizerJni.cpp +14 -0
  116. package/android/src/main/new_arch/ReactNativeEnrichedSpec.cpp +11 -0
  117. package/android/src/main/new_arch/ReactNativeEnrichedSpec.h +15 -0
  118. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputComponentDescriptor.h +35 -0
  119. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.cpp +53 -0
  120. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.h +25 -0
  121. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.cpp +35 -0
  122. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.h +53 -0
  123. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.cpp +9 -0
  124. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.h +24 -0
  125. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h +27 -0
  126. package/android/src/main/res/drawable/broken_image.xml +10 -0
  127. package/cpp/CMakeLists.txt +50 -0
  128. package/cpp/GumboParser/GumboParser.h +34043 -0
  129. package/cpp/README.md +59 -0
  130. package/cpp/parser/GumboNormalizer.c +915 -0
  131. package/cpp/parser/GumboParser.cpp +16 -0
  132. package/cpp/parser/GumboParser.hpp +23 -0
  133. package/cpp/tests/GumboParserTest.cpp +457 -0
  134. package/ios/EnrichedTextInputView.h +53 -0
  135. package/ios/EnrichedTextInputView.mm +2360 -0
  136. package/ios/EnrichedTextInputViewManager.mm +13 -0
  137. package/ios/attributesManager/AttributesManager.h +17 -0
  138. package/ios/attributesManager/AttributesManager.mm +195 -0
  139. package/ios/config/InputConfig.h +104 -0
  140. package/ios/config/InputConfig.mm +664 -0
  141. package/ios/extensions/ColorExtension.h +7 -0
  142. package/ios/extensions/ColorExtension.mm +38 -0
  143. package/ios/extensions/FontExtension.h +11 -0
  144. package/ios/extensions/FontExtension.mm +72 -0
  145. package/ios/extensions/ImageExtension.h +34 -0
  146. package/ios/extensions/ImageExtension.mm +165 -0
  147. package/ios/extensions/LayoutManagerExtension.h +6 -0
  148. package/ios/extensions/LayoutManagerExtension.mm +443 -0
  149. package/ios/extensions/StringExtension.h +15 -0
  150. package/ios/extensions/StringExtension.mm +69 -0
  151. package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
  152. package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
  153. package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
  154. package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
  155. package/ios/generated/ReactNativeEnrichedSpec/Props.cpp +173 -0
  156. package/ios/generated/ReactNativeEnrichedSpec/Props.h +833 -0
  157. package/ios/generated/ReactNativeEnrichedSpec/RCTComponentViewHelpers.h +582 -0
  158. package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
  159. package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
  160. package/ios/generated/ReactNativeEnrichedSpec/States.cpp +16 -0
  161. package/ios/generated/ReactNativeEnrichedSpec/States.h +20 -0
  162. package/ios/inputParser/InputParser.h +11 -0
  163. package/ios/inputParser/InputParser.mm +1463 -0
  164. package/ios/inputTextView/InputTextView.h +6 -0
  165. package/ios/inputTextView/InputTextView.mm +285 -0
  166. package/ios/interfaces/AttributeEntry.h +9 -0
  167. package/ios/interfaces/AttributeEntry.mm +4 -0
  168. package/ios/interfaces/BaseStyleProtocol.h +17 -0
  169. package/ios/interfaces/ImageAttachment.h +11 -0
  170. package/ios/interfaces/ImageAttachment.mm +107 -0
  171. package/ios/interfaces/ImageData.h +10 -0
  172. package/ios/interfaces/ImageData.mm +4 -0
  173. package/ios/interfaces/LinkData.h +11 -0
  174. package/ios/interfaces/LinkData.mm +29 -0
  175. package/ios/interfaces/LinkRegexConfig.h +19 -0
  176. package/ios/interfaces/LinkRegexConfig.mm +37 -0
  177. package/ios/interfaces/MediaAttachment.h +23 -0
  178. package/ios/interfaces/MediaAttachment.mm +31 -0
  179. package/ios/interfaces/MentionParams.h +8 -0
  180. package/ios/interfaces/MentionParams.mm +4 -0
  181. package/ios/interfaces/MentionStyleProps.h +13 -0
  182. package/ios/interfaces/MentionStyleProps.mm +63 -0
  183. package/ios/interfaces/StyleBase.h +36 -0
  184. package/ios/interfaces/StyleBase.mm +256 -0
  185. package/ios/interfaces/StyleHeaders.h +102 -0
  186. package/ios/interfaces/StylePair.h +9 -0
  187. package/ios/interfaces/StylePair.mm +4 -0
  188. package/ios/interfaces/StyleTypeEnum.h +26 -0
  189. package/ios/interfaces/TextDecorationLineEnum.h +6 -0
  190. package/ios/interfaces/TextDecorationLineEnum.mm +4 -0
  191. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +19 -0
  192. package/ios/internals/EnrichedTextInputViewShadowNode.h +44 -0
  193. package/ios/internals/EnrichedTextInputViewShadowNode.mm +103 -0
  194. package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
  195. package/ios/internals/EnrichedTextInputViewState.h +22 -0
  196. package/ios/styles/BlockQuoteStyle.mm +55 -0
  197. package/ios/styles/BoldStyle.mm +37 -0
  198. package/ios/styles/CheckboxListStyle.mm +153 -0
  199. package/ios/styles/CodeBlockStyle.mm +49 -0
  200. package/ios/styles/H1Style.mm +20 -0
  201. package/ios/styles/H2Style.mm +20 -0
  202. package/ios/styles/H3Style.mm +20 -0
  203. package/ios/styles/H4Style.mm +20 -0
  204. package/ios/styles/H5Style.mm +20 -0
  205. package/ios/styles/H6Style.mm +20 -0
  206. package/ios/styles/HeadingStyleBase.mm +65 -0
  207. package/ios/styles/ImageStyle.mm +146 -0
  208. package/ios/styles/InlineCodeStyle.mm +65 -0
  209. package/ios/styles/ItalicStyle.mm +37 -0
  210. package/ios/styles/LinkStyle.mm +532 -0
  211. package/ios/styles/MentionStyle.mm +538 -0
  212. package/ios/styles/OrderedListStyle.mm +86 -0
  213. package/ios/styles/StrikethroughStyle.mm +25 -0
  214. package/ios/styles/UnderlineStyle.mm +24 -0
  215. package/ios/styles/UnorderedListStyle.mm +86 -0
  216. package/ios/utils/CheckboxHitTestUtils.h +10 -0
  217. package/ios/utils/CheckboxHitTestUtils.mm +122 -0
  218. package/ios/utils/DotReplacementUtils.h +10 -0
  219. package/ios/utils/DotReplacementUtils.mm +68 -0
  220. package/ios/utils/KeyboardUtils.h +7 -0
  221. package/ios/utils/KeyboardUtils.mm +31 -0
  222. package/ios/utils/OccurenceUtils.h +44 -0
  223. package/ios/utils/OccurenceUtils.mm +179 -0
  224. package/ios/utils/ParagraphAttributesUtils.h +15 -0
  225. package/ios/utils/ParagraphAttributesUtils.mm +257 -0
  226. package/ios/utils/RangeUtils.h +12 -0
  227. package/ios/utils/RangeUtils.mm +183 -0
  228. package/ios/utils/TextBlockTapGestureRecognizer.h +17 -0
  229. package/ios/utils/TextBlockTapGestureRecognizer.mm +56 -0
  230. package/ios/utils/TextInsertionUtils.h +17 -0
  231. package/ios/utils/TextInsertionUtils.mm +64 -0
  232. package/ios/utils/WordsUtils.h +7 -0
  233. package/ios/utils/WordsUtils.mm +98 -0
  234. package/ios/utils/ZeroWidthSpaceUtils.h +9 -0
  235. package/ios/utils/ZeroWidthSpaceUtils.mm +270 -0
  236. package/lib/module/index.js +4 -0
  237. package/lib/module/index.js.map +1 -0
  238. package/lib/module/native/EnrichedTextInput.js +304 -0
  239. package/lib/module/native/EnrichedTextInput.js.map +1 -0
  240. package/lib/module/package.json +1 -0
  241. package/lib/module/spec/EnrichedTextInputNativeComponent.ts +517 -0
  242. package/lib/module/types.js +4 -0
  243. package/lib/module/types.js.map +1 -0
  244. package/lib/module/utils/EnrichedTextInputDefaultProps.js +12 -0
  245. package/lib/module/utils/EnrichedTextInputDefaultProps.js.map +1 -0
  246. package/lib/module/utils/normalizeHtmlStyle.js +155 -0
  247. package/lib/module/utils/normalizeHtmlStyle.js.map +1 -0
  248. package/lib/module/utils/nullthrows.js +9 -0
  249. package/lib/module/utils/nullthrows.js.map +1 -0
  250. package/lib/module/utils/regexParser.js +46 -0
  251. package/lib/module/utils/regexParser.js.map +1 -0
  252. package/lib/typescript/package.json +1 -0
  253. package/lib/typescript/src/index.d.ts +3 -0
  254. package/lib/typescript/src/index.d.ts.map +1 -0
  255. package/lib/typescript/src/native/EnrichedTextInput.d.ts +3 -0
  256. package/lib/typescript/src/native/EnrichedTextInput.d.ts.map +1 -0
  257. package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts +397 -0
  258. package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts.map +1 -0
  259. package/lib/typescript/src/types.d.ts +447 -0
  260. package/lib/typescript/src/types.d.ts.map +1 -0
  261. package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts +10 -0
  262. package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts.map +1 -0
  263. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts +4 -0
  264. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -0
  265. package/lib/typescript/src/utils/nullthrows.d.ts +2 -0
  266. package/lib/typescript/src/utils/nullthrows.d.ts.map +1 -0
  267. package/lib/typescript/src/utils/regexParser.d.ts +3 -0
  268. package/lib/typescript/src/utils/regexParser.d.ts.map +1 -0
  269. package/package.json +226 -0
  270. package/react-native.config.js +13 -0
  271. package/src/index.tsx +20 -0
  272. package/src/native/EnrichedTextInput.tsx +370 -0
  273. package/src/spec/EnrichedTextInputNativeComponent.ts +517 -0
  274. package/src/types.ts +499 -0
  275. package/src/utils/EnrichedTextInputDefaultProps.ts +9 -0
  276. package/src/utils/normalizeHtmlStyle.ts +199 -0
  277. package/src/utils/nullthrows.ts +7 -0
  278. package/src/utils/regexParser.ts +56 -0
@@ -0,0 +1,1463 @@
1
+ #import "InputParser.h"
2
+ #import "EnrichedTextInputView.h"
3
+ #import "StringExtension.h"
4
+ #import "StyleHeaders.h"
5
+ #import "TextInsertionUtils.h"
6
+ #import "UIView+React.h"
7
+
8
+ #include "GumboParser.hpp"
9
+
10
+ @implementation InputParser {
11
+ EnrichedTextInputView *_input;
12
+ NSInteger _precedingImageCount;
13
+ }
14
+
15
+ - (instancetype)initWithInput:(id)input {
16
+ self = [super init];
17
+ _input = (EnrichedTextInputView *)input;
18
+ _precedingImageCount = 0;
19
+ return self;
20
+ }
21
+
22
+ - (BOOL)isBlockTag:(NSString *)tagName {
23
+ return [tagName isEqualToString:@"ul"] || [tagName isEqualToString:@"ol"] ||
24
+ [tagName isEqualToString:@"blockquote"] ||
25
+ [tagName isEqualToString:@"codeblock"];
26
+ }
27
+
28
+ - (NSString *)parseToHtmlFromRange:(NSRange)range {
29
+ NSInteger offset = range.location;
30
+ NSString *text =
31
+ [_input->textView.textStorage.string substringWithRange:range];
32
+
33
+ if (text.length == 0) {
34
+ return @"<html>\n<p></p>\n</html>";
35
+ }
36
+
37
+ NSMutableString *result = [[NSMutableString alloc] initWithString:@"<html>"];
38
+ NSSet<NSNumber *> *previousActiveStyles = [[NSSet<NSNumber *> alloc] init];
39
+ BOOL newLine = YES;
40
+ BOOL inUnorderedList = NO;
41
+ BOOL inOrderedList = NO;
42
+ BOOL inBlockQuote = NO;
43
+ BOOL inCodeBlock = NO;
44
+ BOOL inCheckboxList = NO;
45
+ unichar lastCharacter = 0;
46
+
47
+ for (int i = 0; i < text.length; i++) {
48
+ NSRange currentRange = NSMakeRange(offset + i, 1);
49
+ NSMutableSet<NSNumber *> *currentActiveStyles =
50
+ [[NSMutableSet<NSNumber *> alloc] init];
51
+ NSMutableDictionary *currentActiveStylesBeginning =
52
+ [[NSMutableDictionary alloc] init];
53
+
54
+ // check each existing style existence
55
+ for (NSNumber *type in _input->stylesDict) {
56
+ StyleBase *style = _input->stylesDict[type];
57
+ if ([style detect:currentRange]) {
58
+ [currentActiveStyles addObject:type];
59
+
60
+ if (![previousActiveStyles member:type]) {
61
+ currentActiveStylesBeginning[type] = [NSNumber numberWithInt:i];
62
+ }
63
+ } else if ([previousActiveStyles member:type]) {
64
+ [currentActiveStylesBeginning removeObjectForKey:type];
65
+ }
66
+ }
67
+
68
+ NSString *currentCharacterStr =
69
+ [_input->textView.textStorage.string substringWithRange:currentRange];
70
+ unichar currentCharacterChar = [_input->textView.textStorage.string
71
+ characterAtIndex:currentRange.location];
72
+
73
+ if ([[NSCharacterSet newlineCharacterSet]
74
+ characterIsMember:currentCharacterChar]) {
75
+ if (newLine) {
76
+ // we can either have an empty list item OR need to close the list and
77
+ // put a BR in such a situation the existence of the list must be
78
+ // checked on 0 length range, not on the newline character
79
+ if (inOrderedList) {
80
+ OrderedListStyle *oStyle = _input->stylesDict[@(OrderedList)];
81
+ BOOL detected = [oStyle detect:NSMakeRange(currentRange.location, 0)];
82
+ if (detected) {
83
+ [result appendString:@"\n<li></li>"];
84
+ } else {
85
+ [result appendString:@"\n</ol>\n<br>"];
86
+ inOrderedList = NO;
87
+ }
88
+ } else if (inUnorderedList) {
89
+ UnorderedListStyle *uStyle = _input->stylesDict[@(UnorderedList)];
90
+ BOOL detected = [uStyle detect:NSMakeRange(currentRange.location, 0)];
91
+ if (detected) {
92
+ [result appendString:@"\n<li></li>"];
93
+ } else {
94
+ [result appendString:@"\n</ul>\n<br>"];
95
+ inUnorderedList = NO;
96
+ }
97
+ } else if (inBlockQuote) {
98
+ BlockQuoteStyle *bqStyle = _input->stylesDict[@(BlockQuote)];
99
+ BOOL detected =
100
+ [bqStyle detect:NSMakeRange(currentRange.location, 0)];
101
+ if (detected) {
102
+ [result appendString:@"\n<br>"];
103
+ } else {
104
+ [result appendString:@"\n</blockquote>\n<br>"];
105
+ inBlockQuote = NO;
106
+ }
107
+ } else if (inCodeBlock) {
108
+ CodeBlockStyle *cbStyle = _input->stylesDict[@(CodeBlock)];
109
+ BOOL detected =
110
+ [cbStyle detect:NSMakeRange(currentRange.location, 0)];
111
+ if (detected) {
112
+ [result appendString:@"\n<br>"];
113
+ } else {
114
+ [result appendString:@"\n</codeblock>\n<br>"];
115
+ inCodeBlock = NO;
116
+ }
117
+ } else if (inCheckboxList) {
118
+ CheckboxListStyle *cbLStyle = _input->stylesDict[@(CheckboxList)];
119
+ BOOL detected =
120
+ [cbLStyle detect:NSMakeRange(currentRange.location, 0)];
121
+ if (detected) {
122
+ BOOL checked = [cbLStyle getCheckboxStateAt:currentRange.location];
123
+ if (checked) {
124
+ [result appendString:@"\n<li checked></li>"];
125
+ } else {
126
+ [result appendString:@"\n<li></li>"];
127
+ }
128
+ } else {
129
+ [result appendString:@"\n</ul>\n<br>"];
130
+ inCheckboxList = NO;
131
+ }
132
+ } else {
133
+ [result appendString:@"\n<br>"];
134
+ }
135
+ } else {
136
+ // newline finishes a paragraph and all style tags need to be closed
137
+ // we use previous styles
138
+ NSArray<NSNumber *> *sortedEndedStyles = [previousActiveStyles
139
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
140
+ sortDescriptorWithKey:@"intValue"
141
+ ascending:NO] ]];
142
+
143
+ // append closing tags
144
+ for (NSNumber *style in sortedEndedStyles) {
145
+ if ([style isEqualToNumber:@([ImageStyle getType])]) {
146
+ continue;
147
+ }
148
+ NSString *tagContent =
149
+ [self tagContentForStyle:style
150
+ openingTag:NO
151
+ location:currentRange.location];
152
+ [result
153
+ appendString:[NSString stringWithFormat:@"</%@>", tagContent]];
154
+ }
155
+
156
+ // append closing paragraph tag
157
+ if ([previousActiveStyles
158
+ containsObject:@([UnorderedListStyle getType])] ||
159
+ [previousActiveStyles
160
+ containsObject:@([OrderedListStyle getType])] ||
161
+ [previousActiveStyles containsObject:@([H1Style getType])] ||
162
+ [previousActiveStyles containsObject:@([H2Style getType])] ||
163
+ [previousActiveStyles containsObject:@([H3Style getType])] ||
164
+ [previousActiveStyles containsObject:@([H4Style getType])] ||
165
+ [previousActiveStyles containsObject:@([H5Style getType])] ||
166
+ [previousActiveStyles containsObject:@([H6Style getType])] ||
167
+ [previousActiveStyles
168
+ containsObject:@([BlockQuoteStyle getType])] ||
169
+ [previousActiveStyles containsObject:@([CodeBlockStyle getType])] ||
170
+ [previousActiveStyles
171
+ containsObject:@([CheckboxListStyle getType])]) {
172
+ // do nothing, proper closing paragraph tags have been already
173
+ // appended
174
+ } else {
175
+ [result appendString:@"</p>"];
176
+ }
177
+ }
178
+
179
+ // clear the previous styles
180
+ previousActiveStyles = [[NSSet<NSNumber *> alloc] init];
181
+
182
+ // next character opens new paragraph
183
+ newLine = YES;
184
+ } else {
185
+ // new line - open the paragraph
186
+ if (newLine) {
187
+ newLine = NO;
188
+
189
+ // handle ending unordered list
190
+ if (inUnorderedList &&
191
+ ![currentActiveStyles
192
+ containsObject:@([UnorderedListStyle getType])]) {
193
+ inUnorderedList = NO;
194
+ [result appendString:@"\n</ul>"];
195
+ }
196
+ // handle ending ordered list
197
+ if (inOrderedList &&
198
+ ![currentActiveStyles
199
+ containsObject:@([OrderedListStyle getType])]) {
200
+ inOrderedList = NO;
201
+ [result appendString:@"\n</ol>"];
202
+ }
203
+ // handle ending blockquotes
204
+ if (inBlockQuote && ![currentActiveStyles
205
+ containsObject:@([BlockQuoteStyle getType])]) {
206
+ inBlockQuote = NO;
207
+ [result appendString:@"\n</blockquote>"];
208
+ }
209
+ // handle ending codeblock
210
+ if (inCodeBlock &&
211
+ ![currentActiveStyles containsObject:@([CodeBlockStyle getType])]) {
212
+ inCodeBlock = NO;
213
+ [result appendString:@"\n</codeblock>"];
214
+ }
215
+ // handle ending checkbox list
216
+ if (inCheckboxList &&
217
+ ![currentActiveStyles
218
+ containsObject:@([CheckboxListStyle getType])]) {
219
+ inCheckboxList = NO;
220
+ [result appendString:@"\n</ul>"];
221
+ }
222
+
223
+ // handle starting unordered list
224
+ if (!inUnorderedList &&
225
+ [currentActiveStyles
226
+ containsObject:@([UnorderedListStyle getType])]) {
227
+ inUnorderedList = YES;
228
+ [result appendString:@"\n<ul>"];
229
+ }
230
+ // handle starting ordered list
231
+ if (!inOrderedList &&
232
+ [currentActiveStyles
233
+ containsObject:@([OrderedListStyle getType])]) {
234
+ inOrderedList = YES;
235
+ [result appendString:@"\n<ol>"];
236
+ }
237
+ // handle starting blockquotes
238
+ if (!inBlockQuote &&
239
+ [currentActiveStyles containsObject:@([BlockQuoteStyle getType])]) {
240
+ inBlockQuote = YES;
241
+ [result appendString:@"\n<blockquote>"];
242
+ }
243
+ // handle starting codeblock
244
+ if (!inCodeBlock &&
245
+ [currentActiveStyles containsObject:@([CodeBlockStyle getType])]) {
246
+ inCodeBlock = YES;
247
+ [result appendString:@"\n<codeblock>"];
248
+ }
249
+ // handle starting checkbox list
250
+ if (!inCheckboxList &&
251
+ [currentActiveStyles
252
+ containsObject:@([CheckboxListStyle getType])]) {
253
+ inCheckboxList = YES;
254
+ [result appendString:@"\n<ul data-type=\"checkbox\">"];
255
+ }
256
+
257
+ // don't add the <p> tag if some paragraph styles are present
258
+ if ([currentActiveStyles
259
+ containsObject:@([UnorderedListStyle getType])] ||
260
+ [currentActiveStyles
261
+ containsObject:@([OrderedListStyle getType])] ||
262
+ [currentActiveStyles containsObject:@([H1Style getType])] ||
263
+ [currentActiveStyles containsObject:@([H2Style getType])] ||
264
+ [currentActiveStyles containsObject:@([H3Style getType])] ||
265
+ [currentActiveStyles containsObject:@([H4Style getType])] ||
266
+ [currentActiveStyles containsObject:@([H5Style getType])] ||
267
+ [currentActiveStyles containsObject:@([H6Style getType])] ||
268
+ [currentActiveStyles containsObject:@([BlockQuoteStyle getType])] ||
269
+ [currentActiveStyles containsObject:@([CodeBlockStyle getType])] ||
270
+ [currentActiveStyles
271
+ containsObject:@([CheckboxListStyle getType])]) {
272
+ [result appendString:@"\n"];
273
+ } else {
274
+ [result appendString:@"\n<p>"];
275
+ }
276
+ }
277
+
278
+ // get styles that have ended
279
+ NSMutableSet<NSNumber *> *endedStyles =
280
+ [previousActiveStyles mutableCopy];
281
+ [endedStyles minusSet:currentActiveStyles];
282
+
283
+ // also finish styles that should be ended becasue they are nested in a
284
+ // style that ended
285
+ NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
286
+ NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
287
+
288
+ for (NSNumber *style in endedStyles) {
289
+ NSInteger styleBeginning =
290
+ [currentActiveStylesBeginning[style] integerValue];
291
+
292
+ for (NSNumber *activeStyle in currentActiveStyles) {
293
+ NSInteger activeStyleBeginning =
294
+ [currentActiveStylesBeginning[activeStyle] integerValue];
295
+
296
+ // we end the styles that began after the currently ended style but
297
+ // not at the "i" (cause the old style ended at exactly "i-1" also the
298
+ // ones that began in the exact same place but are "inner" in relation
299
+ // to them due to StyleTypeEnum integer values
300
+
301
+ if ((activeStyleBeginning > styleBeginning &&
302
+ activeStyleBeginning < i) ||
303
+ (activeStyleBeginning == styleBeginning &&
304
+ activeStyleBeginning<
305
+ i && [activeStyle integerValue]>[style integerValue])) {
306
+ [fixedEndedStyles addObject:activeStyle];
307
+ [stylesToBeReAdded addObject:activeStyle];
308
+ }
309
+ }
310
+ }
311
+
312
+ // if a style begins but there is a style inner to it that is (and was
313
+ // previously) active, it also should be closed and readded
314
+
315
+ // newly added styles
316
+ NSMutableSet *newStyles = [currentActiveStyles mutableCopy];
317
+ [newStyles minusSet:previousActiveStyles];
318
+ // styles that were and still are active
319
+ NSMutableSet *stillActiveStyles = [previousActiveStyles mutableCopy];
320
+ [stillActiveStyles intersectSet:currentActiveStyles];
321
+
322
+ for (NSNumber *style in newStyles) {
323
+ for (NSNumber *ongoingStyle in stillActiveStyles) {
324
+ if ([ongoingStyle integerValue] > [style integerValue]) {
325
+ // the prev style is inner; needs to be closed and re-added later
326
+ [fixedEndedStyles addObject:ongoingStyle];
327
+ [stylesToBeReAdded addObject:ongoingStyle];
328
+ }
329
+ }
330
+ }
331
+
332
+ // they are sorted in a descending order
333
+ NSArray<NSNumber *> *sortedEndedStyles = [fixedEndedStyles
334
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
335
+ sortDescriptorWithKey:@"intValue"
336
+ ascending:NO] ]];
337
+
338
+ // append closing tags
339
+ for (NSNumber *style in sortedEndedStyles) {
340
+ if ([style isEqualToNumber:@([ImageStyle getType])]) {
341
+ continue;
342
+ }
343
+ NSString *tagContent = [self tagContentForStyle:style
344
+ openingTag:NO
345
+ location:currentRange.location];
346
+ [result appendString:[NSString stringWithFormat:@"</%@>", tagContent]];
347
+ }
348
+
349
+ // all styles that have begun: new styles + the ones that need to be
350
+ // re-added they are sorted in a ascending manner to properly keep tags'
351
+ // FILO order
352
+ [newStyles unionSet:stylesToBeReAdded];
353
+ NSArray<NSNumber *> *sortedNewStyles = [newStyles
354
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
355
+ sortDescriptorWithKey:@"intValue"
356
+ ascending:YES] ]];
357
+
358
+ // append opening tags
359
+ for (NSNumber *style in sortedNewStyles) {
360
+ NSString *tagContent = [self tagContentForStyle:style
361
+ openingTag:YES
362
+ location:currentRange.location];
363
+ if ([style isEqualToNumber:@([ImageStyle getType])]) {
364
+ [result
365
+ appendString:[NSString stringWithFormat:@"<%@/>", tagContent]];
366
+ [currentActiveStyles removeObject:@([ImageStyle getType])];
367
+ } else {
368
+ [result appendString:[NSString stringWithFormat:@"<%@>", tagContent]];
369
+ }
370
+ }
371
+
372
+ // append the letter and escape it if needed
373
+ [result appendString:[NSString stringByEscapingHtml:currentCharacterStr]];
374
+
375
+ // save current styles for next character's checks
376
+ previousActiveStyles = currentActiveStyles;
377
+ }
378
+
379
+ // set last character
380
+ lastCharacter = currentCharacterChar;
381
+ }
382
+
383
+ if (![[NSCharacterSet newlineCharacterSet] characterIsMember:lastCharacter]) {
384
+ // not-newline character was last - finish the paragraph
385
+ // close all pending tags
386
+ NSArray<NSNumber *> *sortedEndedStyles = [previousActiveStyles
387
+ sortedArrayUsingDescriptors:@[ [NSSortDescriptor
388
+ sortDescriptorWithKey:@"intValue"
389
+ ascending:NO] ]];
390
+
391
+ // append closing tags
392
+ for (NSNumber *style in sortedEndedStyles) {
393
+ if ([style isEqualToNumber:@([ImageStyle getType])]) {
394
+ continue;
395
+ }
396
+ NSString *tagContent = [self
397
+ tagContentForStyle:style
398
+ openingTag:NO
399
+ location:_input->textView.textStorage.string.length - 1];
400
+ [result appendString:[NSString stringWithFormat:@"</%@>", tagContent]];
401
+ }
402
+
403
+ // finish the paragraph
404
+ // handle ending of some paragraph styles
405
+ if ([previousActiveStyles containsObject:@([UnorderedListStyle getType])]) {
406
+ [result appendString:@"\n</ul>"];
407
+ } else if ([previousActiveStyles
408
+ containsObject:@([OrderedListStyle getType])]) {
409
+ [result appendString:@"\n</ol>"];
410
+ } else if ([previousActiveStyles
411
+ containsObject:@([BlockQuoteStyle getType])]) {
412
+ [result appendString:@"\n</blockquote>"];
413
+ } else if ([previousActiveStyles
414
+ containsObject:@([CodeBlockStyle getType])]) {
415
+ [result appendString:@"\n</codeblock>"];
416
+ } else if ([previousActiveStyles
417
+ containsObject:@([CheckboxListStyle getType])]) {
418
+ [result appendString:@"\n</ul>"];
419
+ } else if ([previousActiveStyles containsObject:@([H1Style getType])] ||
420
+ [previousActiveStyles containsObject:@([H2Style getType])] ||
421
+ [previousActiveStyles containsObject:@([H3Style getType])] ||
422
+ [previousActiveStyles containsObject:@([H4Style getType])] ||
423
+ [previousActiveStyles containsObject:@([H5Style getType])] ||
424
+ [previousActiveStyles containsObject:@([H6Style getType])]) {
425
+ // do nothing, heading closing tag has already been appended
426
+ } else {
427
+ [result appendString:@"</p>"];
428
+ }
429
+ } else {
430
+ // newline character was last - some paragraph styles need to be closed
431
+ if (inUnorderedList) {
432
+ inUnorderedList = NO;
433
+ [result appendString:@"\n</ul>"];
434
+ }
435
+ if (inOrderedList) {
436
+ inOrderedList = NO;
437
+ [result appendString:@"\n</ol>"];
438
+ }
439
+ if (inBlockQuote) {
440
+ inBlockQuote = NO;
441
+ [result appendString:@"\n</blockquote>"];
442
+ }
443
+ if (inCodeBlock) {
444
+ inCodeBlock = NO;
445
+ [result appendString:@"\n</codeblock>"];
446
+ }
447
+ if (inCheckboxList) {
448
+ inCheckboxList = NO;
449
+ [result appendString:@"\n</ul>"];
450
+ }
451
+ }
452
+
453
+ [result appendString:@"\n</html>"];
454
+
455
+ // remove Object Replacement Characters in the very end
456
+ [result replaceOccurrencesOfString:@"\uFFFC"
457
+ withString:@""
458
+ options:0
459
+ range:NSMakeRange(0, result.length)];
460
+
461
+ // remove zero width spaces in the very end
462
+ [result replaceOccurrencesOfString:@"\u200B"
463
+ withString:@""
464
+ options:0
465
+ range:NSMakeRange(0, result.length)];
466
+
467
+ // replace empty <p></p> into <br> in the very end
468
+ [result replaceOccurrencesOfString:@"<p></p>"
469
+ withString:@"<br>"
470
+ options:0
471
+ range:NSMakeRange(0, result.length)];
472
+
473
+ return result;
474
+ }
475
+
476
+ - (NSString *)tagContentForStyle:(NSNumber *)style
477
+ openingTag:(BOOL)openingTag
478
+ location:(NSInteger)location {
479
+ if ([style isEqualToNumber:@([BoldStyle getType])]) {
480
+ return @"b";
481
+ } else if ([style isEqualToNumber:@([ItalicStyle getType])]) {
482
+ return @"i";
483
+ } else if ([style isEqualToNumber:@([ImageStyle getType])]) {
484
+ if (openingTag) {
485
+ ImageStyle *imageStyle =
486
+ (ImageStyle *)_input->stylesDict[@([ImageStyle getType])];
487
+ if (imageStyle != nullptr) {
488
+ ImageData *data = [imageStyle getImageDataAt:location];
489
+ if (data != nullptr && data.uri != nullptr) {
490
+ return [NSString
491
+ stringWithFormat:@"img src=\"%@\" width=\"%f\" height=\"%f\"",
492
+ data.uri, data.width, data.height];
493
+ }
494
+ }
495
+ return @"img";
496
+ } else {
497
+ return @"";
498
+ }
499
+ } else if ([style isEqualToNumber:@([UnderlineStyle getType])]) {
500
+ return @"u";
501
+ } else if ([style isEqualToNumber:@([StrikethroughStyle getType])]) {
502
+ return @"s";
503
+ } else if ([style isEqualToNumber:@([InlineCodeStyle getType])]) {
504
+ return @"code";
505
+ } else if ([style isEqualToNumber:@([LinkStyle getType])]) {
506
+ if (openingTag) {
507
+ LinkStyle *linkStyle =
508
+ (LinkStyle *)_input->stylesDict[@([LinkStyle getType])];
509
+ if (linkStyle != nullptr) {
510
+ LinkData *data = [linkStyle getLinkDataAt:location];
511
+ if (data != nullptr && data.url != nullptr) {
512
+ return [NSString stringWithFormat:@"a href=\"%@\"", data.url];
513
+ }
514
+ }
515
+ return @"a";
516
+ } else {
517
+ return @"a";
518
+ }
519
+ } else if ([style isEqualToNumber:@([MentionStyle getType])]) {
520
+ if (openingTag) {
521
+ MentionStyle *mentionStyle =
522
+ (MentionStyle *)_input->stylesDict[@([MentionStyle getType])];
523
+ if (mentionStyle != nullptr) {
524
+ MentionParams *params = [mentionStyle getMentionParamsAt:location];
525
+ // attributes can theoretically be nullptr
526
+ if (params != nullptr && params.indicator != nullptr &&
527
+ params.text != nullptr) {
528
+ NSMutableString *attrsStr =
529
+ [[NSMutableString alloc] initWithString:@""];
530
+ if (params.attributes != nullptr) {
531
+ // turn attributes to Data and then into dict
532
+ NSData *attrsData =
533
+ [params.attributes dataUsingEncoding:NSUTF8StringEncoding];
534
+ NSError *jsonError;
535
+ NSDictionary *json =
536
+ [NSJSONSerialization JSONObjectWithData:attrsData
537
+ options:0
538
+ error:&jsonError];
539
+ // format dict keys and values into string
540
+ [json enumerateKeysAndObjectsUsingBlock:^(
541
+ id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
542
+ [attrsStr
543
+ appendString:[NSString stringWithFormat:@" %@=\"%@\"",
544
+ (NSString *)key,
545
+ (NSString *)obj]];
546
+ }];
547
+ }
548
+ return [NSString
549
+ stringWithFormat:@"mention text=\"%@\" indicator=\"%@\"%@",
550
+ params.text, params.indicator, attrsStr];
551
+ }
552
+ }
553
+ return @"mention";
554
+ } else {
555
+ return @"mention";
556
+ }
557
+ } else if ([style isEqualToNumber:@([H1Style getType])]) {
558
+ return @"h1";
559
+ } else if ([style isEqualToNumber:@([H2Style getType])]) {
560
+ return @"h2";
561
+ } else if ([style isEqualToNumber:@([H3Style getType])]) {
562
+ return @"h3";
563
+ } else if ([style isEqualToNumber:@([H4Style getType])]) {
564
+ return @"h4";
565
+ } else if ([style isEqualToNumber:@([H5Style getType])]) {
566
+ return @"h5";
567
+ } else if ([style isEqualToNumber:@([H6Style getType])]) {
568
+ return @"h6";
569
+ } else if ([style isEqualToNumber:@([UnorderedListStyle getType])] ||
570
+ [style isEqualToNumber:@([OrderedListStyle getType])]) {
571
+ return @"li";
572
+ } else if ([style isEqualToNumber:@([CheckboxListStyle getType])]) {
573
+ if (openingTag) {
574
+ CheckboxListStyle *checkboxListStyleClass =
575
+ (CheckboxListStyle *)
576
+ _input->stylesDict[@([CheckboxListStyle getType])];
577
+ BOOL checked = [checkboxListStyleClass getCheckboxStateAt:location];
578
+
579
+ if (checked) {
580
+ return @"li checked";
581
+ }
582
+ return @"li";
583
+ } else {
584
+ return @"li";
585
+ }
586
+ } else if ([style isEqualToNumber:@([BlockQuoteStyle getType])] ||
587
+ [style isEqualToNumber:@([CodeBlockStyle getType])]) {
588
+ // blockquotes and codeblock use <p> tags the same way lists use <li>
589
+ return @"p";
590
+ }
591
+ return @"";
592
+ }
593
+
594
+ - (void)replaceWholeFromHtml:(NSString *_Nonnull)html {
595
+ NSArray *processingResult = [self getTextAndStylesFromHtml:html];
596
+ NSString *plainText = (NSString *)processingResult[0];
597
+ NSArray *stylesInfo = (NSArray *)processingResult[1];
598
+
599
+ // reset the text first and reset typing attributes
600
+ _input->textView.text = @"";
601
+ _input->textView.typingAttributes = _input->defaultTypingAttributes;
602
+
603
+ // set new text
604
+ _input->textView.text = plainText;
605
+
606
+ // re-apply the styles
607
+ [self applyProcessedStyles:stylesInfo
608
+ offsetFromBeginning:0
609
+ plainTextLength:plainText.length];
610
+ }
611
+
612
+ - (void)replaceFromHtml:(NSString *_Nonnull)html range:(NSRange)range {
613
+ NSArray *processingResult = [self getTextAndStylesFromHtml:html];
614
+ NSString *plainText = (NSString *)processingResult[0];
615
+ NSArray *stylesInfo = (NSArray *)processingResult[1];
616
+
617
+ // we can use ready replace util
618
+ [TextInsertionUtils replaceText:plainText
619
+ at:range
620
+ additionalAttributes:nil
621
+ input:_input
622
+ withSelection:YES];
623
+
624
+ [self applyProcessedStyles:stylesInfo
625
+ offsetFromBeginning:range.location
626
+ plainTextLength:plainText.length];
627
+ }
628
+
629
+ - (void)insertFromHtml:(NSString *_Nonnull)html location:(NSInteger)location {
630
+ NSArray *processingResult = [self getTextAndStylesFromHtml:html];
631
+ NSString *plainText = (NSString *)processingResult[0];
632
+ NSArray *stylesInfo = (NSArray *)processingResult[1];
633
+
634
+ // same here, insertion utils got our back
635
+ [TextInsertionUtils insertText:plainText
636
+ at:location
637
+ additionalAttributes:nil
638
+ input:_input
639
+ withSelection:YES];
640
+
641
+ [self applyProcessedStyles:stylesInfo
642
+ offsetFromBeginning:location
643
+ plainTextLength:plainText.length];
644
+ }
645
+
646
+ - (void)applyProcessedStyles:(NSArray *)processedStyles
647
+ offsetFromBeginning:(NSInteger)offset
648
+ plainTextLength:(NSUInteger)plainTextLength {
649
+ for (NSArray *arr in processedStyles) {
650
+ // unwrap all info from processed style
651
+ NSNumber *styleType = (NSNumber *)arr[0];
652
+ StylePair *stylePair = (StylePair *)arr[1];
653
+ StyleBase *baseStyle = _input->stylesDict[styleType];
654
+ // range must be taking offest into consideration because processed styles'
655
+ // ranges are relative to only the new text while we need absolute ranges
656
+ // relative to the whole existing text
657
+ NSRange styleRange =
658
+ NSMakeRange(offset + [stylePair.rangeValue rangeValue].location,
659
+ [stylePair.rangeValue rangeValue].length);
660
+
661
+ // of course any changes here need to take blocks and conflicts into
662
+ // consideration
663
+ if ([_input handleStyleBlocksAndConflicts:[[baseStyle class] getType]
664
+ range:styleRange]) {
665
+ if ([styleType isEqualToNumber:@([LinkStyle getType])]) {
666
+ LinkData *linkData = (LinkData *)stylePair.styleValue;
667
+ [((LinkStyle *)baseStyle) addLink:linkData
668
+ range:styleRange
669
+ withSelection:NO];
670
+ } else if ([styleType isEqualToNumber:@([MentionStyle getType])]) {
671
+ MentionParams *params = (MentionParams *)stylePair.styleValue;
672
+ [((MentionStyle *)baseStyle) addMentionAtRange:styleRange
673
+ params:params];
674
+ } else if ([styleType isEqualToNumber:@([ImageStyle getType])]) {
675
+ ImageData *imgData = (ImageData *)stylePair.styleValue;
676
+ [((ImageStyle *)baseStyle) addImageAtRange:styleRange
677
+ imageData:imgData
678
+ withSelection:NO];
679
+ } else if ([styleType isEqualToNumber:@([CheckboxListStyle getType])]) {
680
+ NSDictionary *checkboxStates = (NSDictionary *)stylePair.styleValue;
681
+ CheckboxListStyle *cbLStyle = (CheckboxListStyle *)baseStyle;
682
+
683
+ // First apply the checkbox list style to the entire range with
684
+ // unchecked value
685
+ BOOL shouldAddTypingAttr =
686
+ styleRange.location + styleRange.length == plainTextLength;
687
+ [cbLStyle addWithChecked:NO
688
+ range:styleRange
689
+ withTyping:shouldAddTypingAttr
690
+ withDirtyRange:YES];
691
+
692
+ if (!checkboxStates && checkboxStates.count == 0) {
693
+ continue;
694
+ }
695
+ // Then toggle checked checkboxes
696
+ for (NSNumber *key in checkboxStates) {
697
+ NSUInteger checkboxPosition = offset + [key unsignedIntegerValue];
698
+ BOOL isChecked = [checkboxStates[key] boolValue];
699
+ if (isChecked) {
700
+ [cbLStyle toggleCheckedAt:checkboxPosition];
701
+ }
702
+ }
703
+ } else {
704
+ BOOL shouldAddTypingAttr =
705
+ styleRange.location + styleRange.length == plainTextLength;
706
+ [baseStyle add:styleRange
707
+ withTyping:shouldAddTypingAttr
708
+ withDirtyRange:YES];
709
+ }
710
+ }
711
+ }
712
+ [_input anyTextMayHaveBeenModified];
713
+ }
714
+
715
+ #pragma mark - External HTML normalization
716
+
717
+ /**
718
+ * Normalizes external HTML (from Google Docs, Word, web pages, etc.) into our
719
+ * canonical tag subset using the Gumbo-based C++ normalizer.
720
+ *
721
+ * Converts: strong → b, em → i, span style="font-weight:bold" → b,
722
+ * strips unknown tags while preserving text
723
+ */
724
+ - (NSString *_Nullable)normalizeExternalHtml:(NSString *_Nonnull)html {
725
+ std::string result =
726
+ GumboParser::normalizeHtml(std::string([html UTF8String]));
727
+ if (result.empty())
728
+ return nil;
729
+ return [NSString stringWithUTF8String:result.c_str()];
730
+ }
731
+
732
+ - (NSString *_Nullable)initiallyProcessHtml:(NSString *_Nonnull)html {
733
+ NSString *htmlWithoutSpaces = [self stripExtraWhiteSpacesAndNewlines:html];
734
+ NSString *fixedHtml = nullptr;
735
+
736
+ if (htmlWithoutSpaces.length >= 13) {
737
+ NSString *firstSix =
738
+ [htmlWithoutSpaces substringWithRange:NSMakeRange(0, 6)];
739
+ NSString *lastSeven = [htmlWithoutSpaces
740
+ substringWithRange:NSMakeRange(htmlWithoutSpaces.length - 7, 7)];
741
+
742
+ if ([firstSix isEqualToString:@"<html>"] &&
743
+ [lastSeven isEqualToString:@"</html>"]) {
744
+ // remove html tags, might be with newlines or without them
745
+ fixedHtml = [htmlWithoutSpaces copy];
746
+ // firstly remove newlined html tags if any:
747
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<html>\n"
748
+ withString:@""];
749
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n</html>"
750
+ withString:@""];
751
+ // fallback; remove html tags without their newlines
752
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<html>"
753
+ withString:@""];
754
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"</html>"
755
+ withString:@""];
756
+ } else if (_input->useHtmlNormalizer) {
757
+ // External HTML (from Google Docs, Word, web pages, etc.)
758
+ // Run through the Gumbo-based normalizer to convert arbitrary HTML
759
+ // into our canonical tag subset.
760
+ NSString *normalized = [self normalizeExternalHtml:html];
761
+ if (normalized != nil) {
762
+ fixedHtml = normalized;
763
+ }
764
+ }
765
+
766
+ // Additionally, try getting the content from between body tags if there are
767
+ // some:
768
+
769
+ // Firstly make sure there are no newlines between them.
770
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<body>\n"
771
+ withString:@"<body>"];
772
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"\n</body>"
773
+ withString:@"</body>"];
774
+ // Then, if there actually are body tags, use the content between them.
775
+ NSRange openingBodyRange = [htmlWithoutSpaces rangeOfString:@"<body>"];
776
+ NSRange closingBodyRange = [htmlWithoutSpaces rangeOfString:@"</body>"];
777
+ if (openingBodyRange.length != 0 && closingBodyRange.length != 0) {
778
+ NSInteger newStart = openingBodyRange.location + 6;
779
+ NSInteger newEnd = closingBodyRange.location - 1;
780
+ fixedHtml = [htmlWithoutSpaces
781
+ substringWithRange:NSMakeRange(newStart, newEnd - newStart + 1)];
782
+ }
783
+ }
784
+
785
+ // second processing - try fixing htmls with wrong newlines' setup
786
+ if (fixedHtml != nullptr) {
787
+ // add <br> tag wherever needed
788
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<p></p>"
789
+ withString:@"<br>"];
790
+
791
+ // remove <p> tags inside of <li>
792
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<li><p>"
793
+ withString:@"<li>"];
794
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"</p></li>"
795
+ withString:@"</li>"];
796
+
797
+ // change <br/> to <br>
798
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<br/>"
799
+ withString:@"<br>"];
800
+
801
+ // remove <p> tags around <br>
802
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<p><br>"
803
+ withString:@"<br>"];
804
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<br></p>"
805
+ withString:@"<br>"];
806
+
807
+ // add <br> tags inside empty blockquote and codeblock tags
808
+ fixedHtml = [fixedHtml
809
+ stringByReplacingOccurrencesOfString:@"<blockquote></blockquote>"
810
+ withString:@"<blockquote><br></"
811
+ @"blockquote>"];
812
+ fixedHtml = [fixedHtml
813
+ stringByReplacingOccurrencesOfString:@"<codeblock></codeblock>"
814
+ withString:@"<codeblock><br></codeblock>"];
815
+
816
+ // remove empty ul and ol tags
817
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<ul></ul>"
818
+ withString:@""];
819
+ fixedHtml = [fixedHtml
820
+ stringByReplacingOccurrencesOfString:@"<ul data-type=\"checkbox\"></ul>"
821
+ withString:@""];
822
+ fixedHtml = [fixedHtml stringByReplacingOccurrencesOfString:@"<ol></ol>"
823
+ withString:@""];
824
+
825
+ // tags that have to be in separate lines
826
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<br>"
827
+ inString:fixedHtml
828
+ leading:YES
829
+ trailing:YES];
830
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<ul>"
831
+ inString:fixedHtml
832
+ leading:YES
833
+ trailing:YES];
834
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</ul>"
835
+ inString:fixedHtml
836
+ leading:YES
837
+ trailing:YES];
838
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<ol>"
839
+ inString:fixedHtml
840
+ leading:YES
841
+ trailing:YES];
842
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</ol>"
843
+ inString:fixedHtml
844
+ leading:YES
845
+ trailing:YES];
846
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<blockquote>"
847
+ inString:fixedHtml
848
+ leading:YES
849
+ trailing:YES];
850
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</blockquote>"
851
+ inString:fixedHtml
852
+ leading:YES
853
+ trailing:YES];
854
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<codeblock>"
855
+ inString:fixedHtml
856
+ leading:YES
857
+ trailing:YES];
858
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</codeblock>"
859
+ inString:fixedHtml
860
+ leading:YES
861
+ trailing:YES];
862
+
863
+ // line opening tags
864
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<p>"
865
+ inString:fixedHtml
866
+ leading:YES
867
+ trailing:NO];
868
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<li>"
869
+ inString:fixedHtml
870
+ leading:YES
871
+ trailing:NO];
872
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<li checked>"
873
+ inString:fixedHtml
874
+ leading:YES
875
+ trailing:NO];
876
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<h1>"
877
+ inString:fixedHtml
878
+ leading:YES
879
+ trailing:NO];
880
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<h2>"
881
+ inString:fixedHtml
882
+ leading:YES
883
+ trailing:NO];
884
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<h3>"
885
+ inString:fixedHtml
886
+ leading:YES
887
+ trailing:NO];
888
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<h4>"
889
+ inString:fixedHtml
890
+ leading:YES
891
+ trailing:NO];
892
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<h5>"
893
+ inString:fixedHtml
894
+ leading:YES
895
+ trailing:NO];
896
+ fixedHtml = [self stringByAddingNewlinesToTag:@"<h6>"
897
+ inString:fixedHtml
898
+ leading:YES
899
+ trailing:NO];
900
+
901
+ // line closing tags
902
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</p>"
903
+ inString:fixedHtml
904
+ leading:NO
905
+ trailing:YES];
906
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</li>"
907
+ inString:fixedHtml
908
+ leading:NO
909
+ trailing:YES];
910
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</h1>"
911
+ inString:fixedHtml
912
+ leading:NO
913
+ trailing:YES];
914
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</h2>"
915
+ inString:fixedHtml
916
+ leading:NO
917
+ trailing:YES];
918
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</h3>"
919
+ inString:fixedHtml
920
+ leading:NO
921
+ trailing:YES];
922
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</h4>"
923
+ inString:fixedHtml
924
+ leading:NO
925
+ trailing:YES];
926
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</h5>"
927
+ inString:fixedHtml
928
+ leading:NO
929
+ trailing:YES];
930
+ fixedHtml = [self stringByAddingNewlinesToTag:@"</h6>"
931
+ inString:fixedHtml
932
+ leading:NO
933
+ trailing:YES];
934
+
935
+ // this is more like a hack but for some reason the last <br> in
936
+ // <blockquote> and <codeblock> are not properly changed into zero width
937
+ // space so we do that manually here
938
+ fixedHtml = [fixedHtml
939
+ stringByReplacingOccurrencesOfString:@"<br>\n</blockquote>"
940
+ withString:@"<p>\u200B</p>\n</blockquote>"];
941
+ fixedHtml = [fixedHtml
942
+ stringByReplacingOccurrencesOfString:@"<br>\n</codeblock>"
943
+ withString:@"<p>\u200B</p>\n</codeblock>"];
944
+
945
+ // replace "<br>" at the end with "<br>\n" if input is not empty to properly
946
+ // handle last <br> in html
947
+ if ([fixedHtml hasSuffix:@"<br>"] && fixedHtml.length != 4) {
948
+ fixedHtml = [fixedHtml stringByAppendingString:@"\n"];
949
+ }
950
+ }
951
+
952
+ return fixedHtml;
953
+ }
954
+
955
+ /**
956
+ * Prepares HTML for the parser by stripping extraneous whitespace and newlines
957
+ * from structural tags, while preserving them within text content.
958
+ *
959
+ * APPROACH:
960
+ * This function treats the HTML as having two distinct states:
961
+ * 1. Structure Mode (Depth == 0): We are inside or between container tags (like
962
+ * blockquote, ul, codeblock). In this mode whitespace and newlines are
963
+ * considered layout artifacts and are REMOVED to prevent the parser from
964
+ * creating unwanted spaces.
965
+ * 2. Content Mode (Depth > 0): We are inside a text-containing tag (like p,
966
+ * b, li). In this mode, all whitespace is PRESERVED exactly as is, ensuring
967
+ * that sentences and inline formatting remain readable.
968
+ *
969
+ * The function iterates character-by-character, using a depth counter to track
970
+ * nesting levels of the specific tags defined in `textTags`.
971
+ *
972
+ * IMPORTANT:
973
+ * The `textTags` set acts as a whitelist for "Content Mode". If you add support
974
+ * for a new HTML tag that contains visible text (e.g., h4, h5, h6),
975
+ * you MUST add it to the `textTags` set below.
976
+ */
977
+ - (NSString *)stripExtraWhiteSpacesAndNewlines:(NSString *)html {
978
+ NSSet *textTags = [NSSet setWithObjects:@"p", @"h1", @"h2", @"h3", @"h4",
979
+ @"h5", @"h6", @"li", @"b", @"a", @"s",
980
+ @"mention", @"code", @"u", @"i", nil];
981
+
982
+ NSMutableString *output = [NSMutableString stringWithCapacity:html.length];
983
+ NSMutableString *currentTagBuffer = [NSMutableString string];
984
+ NSCharacterSet *whitespaceAndNewlineSet =
985
+ [NSCharacterSet whitespaceAndNewlineCharacterSet];
986
+
987
+ BOOL isReadingTag = NO;
988
+ NSInteger textDepth = 0;
989
+
990
+ for (NSUInteger i = 0; i < html.length; i++) {
991
+ unichar c = [html characterAtIndex:i];
992
+
993
+ if (c == '<') {
994
+ isReadingTag = YES;
995
+ [currentTagBuffer setString:@""];
996
+ [output appendString:@"<"];
997
+ } else if (c == '>') {
998
+ isReadingTag = NO;
999
+ [output appendString:@">"];
1000
+
1001
+ NSString *fullTag = [currentTagBuffer lowercaseString];
1002
+
1003
+ NSString *cleanName = [fullTag
1004
+ stringByTrimmingCharactersInSet:
1005
+ [NSCharacterSet characterSetWithCharactersInString:@"/"]];
1006
+ NSArray *parts =
1007
+ [cleanName componentsSeparatedByCharactersInSet:
1008
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
1009
+ NSString *tagName = parts.firstObject;
1010
+
1011
+ if (![textTags containsObject:tagName]) {
1012
+ continue;
1013
+ }
1014
+
1015
+ if ([fullTag hasPrefix:@"/"]) {
1016
+ textDepth--;
1017
+ if (textDepth < 0)
1018
+ textDepth = 0;
1019
+ } else {
1020
+ // Opening tag (e.g. <h1>) -> Enter Text Mode
1021
+ // (Ignore self-closing tags like <img/> if they happen to be in the
1022
+ // list)
1023
+ if (![fullTag hasSuffix:@"/"]) {
1024
+ textDepth++;
1025
+ }
1026
+ }
1027
+ } else {
1028
+ if (isReadingTag) {
1029
+ [currentTagBuffer appendFormat:@"%C", c];
1030
+ [output appendFormat:@"%C", c];
1031
+ continue;
1032
+ }
1033
+
1034
+ if (textDepth > 0) {
1035
+ [output appendFormat:@"%C", c];
1036
+ } else {
1037
+ if (![whitespaceAndNewlineSet characterIsMember:c]) {
1038
+ [output appendFormat:@"%C", c];
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ return output;
1045
+ }
1046
+
1047
+ - (NSString *)stringByAddingNewlinesToTag:(NSString *)tag
1048
+ inString:(NSString *)html
1049
+ leading:(BOOL)leading
1050
+ trailing:(BOOL)trailing {
1051
+ NSString *str = [html copy];
1052
+ if (leading) {
1053
+ NSString *formattedTag = [NSString stringWithFormat:@">%@", tag];
1054
+ NSString *formattedNewTag = [NSString stringWithFormat:@">\n%@", tag];
1055
+ str = [str stringByReplacingOccurrencesOfString:formattedTag
1056
+ withString:formattedNewTag];
1057
+ }
1058
+ if (trailing) {
1059
+ NSString *formattedTag = [NSString stringWithFormat:@"%@<", tag];
1060
+ NSString *formattedNewTag = [NSString stringWithFormat:@"%@\n<", tag];
1061
+ str = [str stringByReplacingOccurrencesOfString:formattedTag
1062
+ withString:formattedNewTag];
1063
+ }
1064
+ return str;
1065
+ }
1066
+
1067
+ - (void)finalizeTagEntry:(NSMutableString *)tagName
1068
+ ongoingTags:(NSMutableDictionary *)ongoingTags
1069
+ initiallyProcessedTags:(NSMutableArray *)processedTags
1070
+ plainText:(NSMutableString *)plainText {
1071
+ NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
1072
+
1073
+ NSArray *tagData = ongoingTags[tagName];
1074
+ NSInteger tagLocation = [((NSNumber *)tagData[0]) intValue];
1075
+
1076
+ // 'tagLocation' is an index based on 'plainText' which currently only holds
1077
+ // raw text.
1078
+ //
1079
+ // Since 'plainText' does not yet contain the special placeholders for images,
1080
+ // the indices for any text following an image are lower than they will be
1081
+ // in the final NSTextStorage.
1082
+ //
1083
+ // We add '_precedingImageCount' to shift the start index forward, aligning
1084
+ // this style's range with the actual position in the final text (where each
1085
+ // image adds 1 character).
1086
+ NSRange tagRange = NSMakeRange(tagLocation + _precedingImageCount,
1087
+ plainText.length - tagLocation);
1088
+
1089
+ [tagEntry addObject:[tagName copy]];
1090
+ [tagEntry addObject:[NSValue valueWithRange:tagRange]];
1091
+ if (tagData.count > 1) {
1092
+ [tagEntry addObject:[(NSString *)tagData[1] copy]];
1093
+ }
1094
+
1095
+ [processedTags addObject:tagEntry];
1096
+ [ongoingTags removeObjectForKey:tagName];
1097
+
1098
+ if ([tagName isEqualToString:@"img"]) {
1099
+ _precedingImageCount++;
1100
+ }
1101
+ }
1102
+
1103
+ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
1104
+ NSMutableString *plainText = [[NSMutableString alloc] initWithString:@""];
1105
+ NSMutableDictionary *ongoingTags = [[NSMutableDictionary alloc] init];
1106
+ NSMutableArray *initiallyProcessedTags = [[NSMutableArray alloc] init];
1107
+ NSMutableDictionary *checkboxStates = [[NSMutableDictionary alloc] init];
1108
+ BOOL insideCheckboxList = NO;
1109
+ _precedingImageCount = 0;
1110
+ BOOL insideTag = NO;
1111
+ BOOL gettingTagName = NO;
1112
+ BOOL gettingTagParams = NO;
1113
+ BOOL closingTag = NO;
1114
+ NSMutableString *currentTagName =
1115
+ [[NSMutableString alloc] initWithString:@""];
1116
+ NSMutableString *currentTagParams =
1117
+ [[NSMutableString alloc] initWithString:@""];
1118
+ NSDictionary *htmlEntitiesDict =
1119
+ [NSString getEscapedCharactersInfoFrom:fixedHtml];
1120
+
1121
+ // firstly, extract text and initially processed tags
1122
+ for (int i = 0; i < fixedHtml.length; i++) {
1123
+ NSString *currentCharacterStr =
1124
+ [fixedHtml substringWithRange:NSMakeRange(i, 1)];
1125
+ unichar currentCharacterChar = [fixedHtml characterAtIndex:i];
1126
+
1127
+ if (currentCharacterChar == '<') {
1128
+ // opening the tag, mark that we are inside and getting its name
1129
+ insideTag = YES;
1130
+ gettingTagName = YES;
1131
+ } else if (currentCharacterChar == '>') {
1132
+ // finishing some tag, no longer marked as inside or getting its
1133
+ // name/params
1134
+ insideTag = NO;
1135
+ gettingTagName = NO;
1136
+ gettingTagParams = NO;
1137
+
1138
+ BOOL isSelfClosing = NO;
1139
+
1140
+ // Check if params ended with '/' (e.g. <img src="" />)
1141
+ if ([currentTagParams hasSuffix:@"/"]) {
1142
+ [currentTagParams
1143
+ deleteCharactersInRange:NSMakeRange(currentTagParams.length - 1,
1144
+ 1)];
1145
+ isSelfClosing = YES;
1146
+ }
1147
+
1148
+ if ([currentTagName isEqualToString:@"p"] ||
1149
+ [currentTagName isEqualToString:@"br"]) {
1150
+ // do nothing, we don't include these tags in styles
1151
+ } else if ([currentTagName isEqualToString:@"li"]) {
1152
+ // Only track checkbox state if we're inside a checkbox list
1153
+ if (insideCheckboxList && !closingTag) {
1154
+ BOOL isChecked = [currentTagParams containsString:@"checked"];
1155
+ checkboxStates[@(plainText.length)] = @(isChecked);
1156
+ }
1157
+ } else if (!closingTag) {
1158
+ // we finish opening tag - get its location and optionally params and
1159
+ // put them under tag name key in ongoingTags
1160
+ NSMutableArray *tagArr = [[NSMutableArray alloc] init];
1161
+ [tagArr addObject:[NSNumber numberWithInteger:plainText.length]];
1162
+ if (currentTagParams.length > 0) {
1163
+ [tagArr addObject:[currentTagParams copy]];
1164
+ }
1165
+ ongoingTags[currentTagName] = tagArr;
1166
+
1167
+ // Check if this is a checkbox list
1168
+ if ([currentTagName isEqualToString:@"ul"] &&
1169
+ [self isUlCheckboxList:currentTagParams]) {
1170
+ insideCheckboxList = YES;
1171
+ }
1172
+
1173
+ // skip one newline if it was added after opening tags that are in
1174
+ // separate lines
1175
+ if ([self isBlockTag:currentTagName] && i + 1 < fixedHtml.length &&
1176
+ [[NSCharacterSet newlineCharacterSet]
1177
+ characterIsMember:[fixedHtml characterAtIndex:i + 1]]) {
1178
+ i += 1;
1179
+ }
1180
+
1181
+ if (isSelfClosing) {
1182
+ [self finalizeTagEntry:currentTagName
1183
+ ongoingTags:ongoingTags
1184
+ initiallyProcessedTags:initiallyProcessedTags
1185
+ plainText:plainText];
1186
+ }
1187
+ } else {
1188
+ // we finish closing tags - pack tag name, tag range and optionally tag
1189
+ // params into an entry that goes inside initiallyProcessedTags
1190
+
1191
+ // Check if we're closing a checkbox list by looking at the params
1192
+ if ([currentTagName isEqualToString:@"ul"] &&
1193
+ [self isUlCheckboxList:currentTagParams]) {
1194
+ insideCheckboxList = NO;
1195
+ }
1196
+
1197
+ BOOL isBlockTag = [self isBlockTag:currentTagName];
1198
+
1199
+ // skip one newline if it was added before some closing tags that are
1200
+ // in separate lines
1201
+ if (isBlockTag && plainText.length > 0 &&
1202
+ [[NSCharacterSet newlineCharacterSet]
1203
+ characterIsMember:[plainText
1204
+ characterAtIndex:plainText.length - 1]]) {
1205
+ plainText = [[plainText
1206
+ substringWithRange:NSMakeRange(0, plainText.length - 1)]
1207
+ mutableCopy];
1208
+ }
1209
+
1210
+ [self finalizeTagEntry:currentTagName
1211
+ ongoingTags:ongoingTags
1212
+ initiallyProcessedTags:initiallyProcessedTags
1213
+ plainText:plainText];
1214
+ }
1215
+ // post-tag cleanup
1216
+ closingTag = NO;
1217
+ currentTagName = [[NSMutableString alloc] initWithString:@""];
1218
+ currentTagParams = [[NSMutableString alloc] initWithString:@""];
1219
+ } else {
1220
+ if (!insideTag) {
1221
+ // no tags logic - just append the right text
1222
+
1223
+ // html entity on the index; use unescaped character and forward
1224
+ // iterator accordingly
1225
+ NSArray *entityInfo = htmlEntitiesDict[@(i)];
1226
+ if (entityInfo != nullptr) {
1227
+ NSString *escaped = entityInfo[0];
1228
+ NSString *unescaped = entityInfo[1];
1229
+ [plainText appendString:unescaped];
1230
+ // the iterator will forward by 1 itself
1231
+ i += escaped.length - 1;
1232
+ } else {
1233
+ [plainText appendString:currentCharacterStr];
1234
+ }
1235
+ } else {
1236
+ if (gettingTagName) {
1237
+ if (currentCharacterChar == ' ') {
1238
+ // no longer getting tag name - switch to params
1239
+ gettingTagName = NO;
1240
+ gettingTagParams = YES;
1241
+ } else if (currentCharacterChar == '/') {
1242
+ // mark that the tag is closing
1243
+ closingTag = YES;
1244
+ } else {
1245
+ // append next tag char
1246
+ [currentTagName appendString:currentCharacterStr];
1247
+ }
1248
+ } else if (gettingTagParams) {
1249
+ // append next tag params char
1250
+ [currentTagParams appendString:currentCharacterStr];
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // process tags into proper StyleType + StylePair values
1257
+ NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
1258
+
1259
+ for (NSArray *arr in initiallyProcessedTags) {
1260
+ NSString *tagName = (NSString *)arr[0];
1261
+ NSValue *tagRangeValue = (NSValue *)arr[1];
1262
+ NSMutableString *params = [[NSMutableString alloc] initWithString:@""];
1263
+ if (arr.count > 2) {
1264
+ [params appendString:(NSString *)arr[2]];
1265
+ }
1266
+
1267
+ NSMutableArray *styleArr = [[NSMutableArray alloc] init];
1268
+ StylePair *stylePair = [[StylePair alloc] init];
1269
+ if ([tagName isEqualToString:@"b"]) {
1270
+ [styleArr addObject:@([BoldStyle getType])];
1271
+ } else if ([tagName isEqualToString:@"i"]) {
1272
+ [styleArr addObject:@([ItalicStyle getType])];
1273
+ } else if ([tagName isEqualToString:@"img"]) {
1274
+ NSRegularExpression *srcRegex =
1275
+ [NSRegularExpression regularExpressionWithPattern:@"src=\"([^\"]+)\""
1276
+ options:0
1277
+ error:nullptr];
1278
+ NSTextCheckingResult *match =
1279
+ [srcRegex firstMatchInString:params
1280
+ options:0
1281
+ range:NSMakeRange(0, params.length)];
1282
+
1283
+ if (match == nullptr) {
1284
+ continue;
1285
+ }
1286
+
1287
+ NSRange srcRange = match.range;
1288
+ [styleArr addObject:@([ImageStyle getType])];
1289
+ // cut only the uri from the src="..." string
1290
+ NSString *uri =
1291
+ [params substringWithRange:NSMakeRange(srcRange.location + 5,
1292
+ srcRange.length - 6)];
1293
+ ImageData *imageData = [[ImageData alloc] init];
1294
+ imageData.uri = uri;
1295
+
1296
+ NSRegularExpression *widthRegex = [NSRegularExpression
1297
+ regularExpressionWithPattern:@"width=\"([0-9.]+)\""
1298
+ options:0
1299
+ error:nil];
1300
+ NSTextCheckingResult *widthMatch =
1301
+ [widthRegex firstMatchInString:params
1302
+ options:0
1303
+ range:NSMakeRange(0, params.length)];
1304
+
1305
+ if (widthMatch) {
1306
+ NSString *widthString =
1307
+ [params substringWithRange:[widthMatch rangeAtIndex:1]];
1308
+ imageData.width = [widthString floatValue];
1309
+ }
1310
+
1311
+ NSRegularExpression *heightRegex = [NSRegularExpression
1312
+ regularExpressionWithPattern:@"height=\"([0-9.]+)\""
1313
+ options:0
1314
+ error:nil];
1315
+ NSTextCheckingResult *heightMatch =
1316
+ [heightRegex firstMatchInString:params
1317
+ options:0
1318
+ range:NSMakeRange(0, params.length)];
1319
+
1320
+ if (heightMatch) {
1321
+ NSString *heightString =
1322
+ [params substringWithRange:[heightMatch rangeAtIndex:1]];
1323
+ imageData.height = [heightString floatValue];
1324
+ }
1325
+
1326
+ stylePair.styleValue = imageData;
1327
+ } else if ([tagName isEqualToString:@"u"]) {
1328
+ [styleArr addObject:@([UnderlineStyle getType])];
1329
+ } else if ([tagName isEqualToString:@"s"]) {
1330
+ [styleArr addObject:@([StrikethroughStyle getType])];
1331
+ } else if ([tagName isEqualToString:@"code"]) {
1332
+ [styleArr addObject:@([InlineCodeStyle getType])];
1333
+ } else if ([tagName isEqualToString:@"a"]) {
1334
+ NSRegularExpression *hrefRegex =
1335
+ [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
1336
+ options:0
1337
+ error:nullptr];
1338
+ NSTextCheckingResult *match =
1339
+ [hrefRegex firstMatchInString:params
1340
+ options:0
1341
+ range:NSMakeRange(0, params.length)];
1342
+
1343
+ if (match == nullptr) {
1344
+ // same as on Android, no href (or empty href) equals no link style
1345
+ continue;
1346
+ }
1347
+
1348
+ NSRange hrefRange = match.range;
1349
+ [styleArr addObject:@([LinkStyle getType])];
1350
+ // cut only the url from the href="..." string
1351
+ NSString *url =
1352
+ [params substringWithRange:NSMakeRange(hrefRange.location + 6,
1353
+ hrefRange.length - 7)];
1354
+ NSString *text = [plainText substringWithRange:tagRangeValue.rangeValue];
1355
+
1356
+ LinkData *linkData = [[LinkData alloc] init];
1357
+ linkData.url = url;
1358
+ linkData.text = text;
1359
+ linkData.isManual = ![text isEqualToString:url];
1360
+
1361
+ stylePair.styleValue = linkData;
1362
+ } else if ([tagName isEqualToString:@"mention"]) {
1363
+ [styleArr addObject:@([MentionStyle getType])];
1364
+ // extract html expression into dict using some regex
1365
+ NSMutableDictionary *paramsDict = [[NSMutableDictionary alloc] init];
1366
+ NSString *pattern = @"(\\w+)=(['\"])(.*?)\\2";
1367
+ NSRegularExpression *regex =
1368
+ [NSRegularExpression regularExpressionWithPattern:pattern
1369
+ options:0
1370
+ error:nil];
1371
+
1372
+ [regex enumerateMatchesInString:params
1373
+ options:0
1374
+ range:NSMakeRange(0, params.length)
1375
+ usingBlock:^(NSTextCheckingResult *_Nullable result,
1376
+ NSMatchingFlags flags,
1377
+ BOOL *_Nonnull stop) {
1378
+ if (result.numberOfRanges == 4) {
1379
+ NSString *key = [params
1380
+ substringWithRange:[result rangeAtIndex:1]];
1381
+ NSString *value = [params
1382
+ substringWithRange:[result rangeAtIndex:3]];
1383
+ paramsDict[key] = value;
1384
+ }
1385
+ }];
1386
+
1387
+ MentionParams *mentionParams = [[MentionParams alloc] init];
1388
+ mentionParams.text = paramsDict[@"text"];
1389
+ mentionParams.indicator = paramsDict[@"indicator"];
1390
+
1391
+ [paramsDict removeObjectsForKeys:@[ @"text", @"indicator" ]];
1392
+ NSError *error;
1393
+ NSData *attrsData = [NSJSONSerialization dataWithJSONObject:paramsDict
1394
+ options:0
1395
+ error:&error];
1396
+ NSString *formattedAttrsString =
1397
+ [[NSString alloc] initWithData:attrsData
1398
+ encoding:NSUTF8StringEncoding];
1399
+ mentionParams.attributes = formattedAttrsString;
1400
+
1401
+ stylePair.styleValue = mentionParams;
1402
+ } else if ([tagName isEqualToString:@"h1"]) {
1403
+ [styleArr addObject:@([H1Style getType])];
1404
+ } else if ([tagName isEqualToString:@"h2"]) {
1405
+ [styleArr addObject:@([H2Style getType])];
1406
+ } else if ([tagName isEqualToString:@"h3"]) {
1407
+ [styleArr addObject:@([H3Style getType])];
1408
+ } else if ([tagName isEqualToString:@"h4"]) {
1409
+ [styleArr addObject:@([H4Style getType])];
1410
+ } else if ([tagName isEqualToString:@"h5"]) {
1411
+ [styleArr addObject:@([H5Style getType])];
1412
+ } else if ([tagName isEqualToString:@"h6"]) {
1413
+ [styleArr addObject:@([H6Style getType])];
1414
+ } else if ([tagName isEqualToString:@"ul"]) {
1415
+ if ([self isUlCheckboxList:params]) {
1416
+ [styleArr addObject:@([CheckboxListStyle getType])];
1417
+ stylePair.styleValue =
1418
+ [self prepareCheckboxListStyleValue:tagRangeValue
1419
+ checkboxStates:checkboxStates];
1420
+ } else {
1421
+ [styleArr addObject:@([UnorderedListStyle getType])];
1422
+ }
1423
+ } else if ([tagName isEqualToString:@"ol"]) {
1424
+ [styleArr addObject:@([OrderedListStyle getType])];
1425
+ } else if ([tagName isEqualToString:@"blockquote"]) {
1426
+ [styleArr addObject:@([BlockQuoteStyle getType])];
1427
+ } else if ([tagName isEqualToString:@"codeblock"]) {
1428
+ [styleArr addObject:@([CodeBlockStyle getType])];
1429
+ } else {
1430
+ // some other external tags like span just don't get put into the
1431
+ // processed styles
1432
+ continue;
1433
+ }
1434
+
1435
+ stylePair.rangeValue = tagRangeValue;
1436
+ [styleArr addObject:stylePair];
1437
+ [processedStyles addObject:styleArr];
1438
+ }
1439
+
1440
+ return @[ plainText, processedStyles ];
1441
+ }
1442
+
1443
+ - (BOOL)isUlCheckboxList:(NSString *)params {
1444
+ return ([params containsString:@"data-type=\"checkbox\""] ||
1445
+ [params containsString:@"data-type='checkbox'"]);
1446
+ }
1447
+
1448
+ - (NSDictionary *)prepareCheckboxListStyleValue:(NSValue *)rangeValue
1449
+ checkboxStates:(NSDictionary *)checkboxStates {
1450
+ NSRange range = [rangeValue rangeValue];
1451
+ NSMutableDictionary *statesInRange = [[NSMutableDictionary alloc] init];
1452
+
1453
+ for (NSNumber *key in checkboxStates) {
1454
+ NSUInteger pos = [key unsignedIntegerValue];
1455
+ if (pos >= range.location && pos < range.location + range.length) {
1456
+ [statesInRange setObject:checkboxStates[key] forKey:key];
1457
+ }
1458
+ }
1459
+
1460
+ return statesInRange;
1461
+ }
1462
+
1463
+ @end