@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.
- package/LICENSE +20 -0
- package/README.md +343 -0
- package/ReactNativeEnriched.podspec +31 -0
- package/android/build.gradle +106 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +197 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +72 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.cpp +173 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.h +833 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/lint.gradle +70 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/swmansion/enriched/ReactNativeEnrichedPackage.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt +126 -0
- package/android/src/main/java/com/swmansion/enriched/common/CheckboxDrawable.kt +81 -0
- package/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/common/EnrichedStyle.kt +57 -0
- package/android/src/main/java/com/swmansion/enriched/common/ForceRedrawSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/common/GumboNormalizer.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/MentionStyle.kt +7 -0
- package/android/src/main/java/com/swmansion/enriched/common/ResourceManager.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +956 -0
- package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +79 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBlockQuoteSpan.kt +53 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBoldSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt +92 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCodeBlockSpan.kt +81 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH1Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH2Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH3Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH4Span.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH5Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH6Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt +184 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedInlineCodeSpan.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedItalicSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedLinkSpan.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedMentionSpan.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedOrderedListSpan.kt +79 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedStrikeThroughSpan.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnderlineSpan.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedBlockSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedHeadingSpan.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedInlineSpan.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedParagraphSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedSpan.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputConnectionWrapper.kt +140 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +83 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +1120 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewLayoutManager.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +478 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt +225 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/MentionHandler.kt +55 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeHtmlEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeSelectionEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeStateEvent.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeTextEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputBlurEvent.kt +25 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputFocusEvent.kt +25 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputKeyPressEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnLinkDetectedEvent.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionDetectedEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionEvent.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnPasteImagesEvent.kt +47 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnRequestHtmlResultEvent.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnSubmitEditingEvent.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBlockQuoteSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBoldSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCheckboxListSpan.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCodeBlockSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH1Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH2Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH3Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH4Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH5Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH6Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputInlineCodeSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputItalicSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputLinkSpan.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputMentionSpan.kt +18 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputOrderedListSpan.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputStrikeThroughSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnderlineSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnorderedListSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt +44 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedSpans.kt +241 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/interfaces/EnrichedInputSpan.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt +372 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +164 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +263 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +434 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +394 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedEditableFactory.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +320 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpanState.kt +310 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +106 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannableStringBuilder.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/RichContentReceiver.kt +127 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +106 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedSpanWatcher.kt +107 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt +74 -0
- package/android/src/main/new_arch/CMakeLists.txt +62 -0
- package/android/src/main/new_arch/GumboNormalizerJni.cpp +14 -0
- package/android/src/main/new_arch/ReactNativeEnrichedSpec.cpp +11 -0
- package/android/src/main/new_arch/ReactNativeEnrichedSpec.h +15 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputComponentDescriptor.h +35 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.cpp +53 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.h +25 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.cpp +35 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.h +53 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.cpp +9 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.h +24 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h +27 -0
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/cpp/CMakeLists.txt +50 -0
- package/cpp/GumboParser/GumboParser.h +34043 -0
- package/cpp/README.md +59 -0
- package/cpp/parser/GumboNormalizer.c +915 -0
- package/cpp/parser/GumboParser.cpp +16 -0
- package/cpp/parser/GumboParser.hpp +23 -0
- package/cpp/tests/GumboParserTest.cpp +457 -0
- package/ios/EnrichedTextInputView.h +53 -0
- package/ios/EnrichedTextInputView.mm +2360 -0
- package/ios/EnrichedTextInputViewManager.mm +13 -0
- package/ios/attributesManager/AttributesManager.h +17 -0
- package/ios/attributesManager/AttributesManager.mm +195 -0
- package/ios/config/InputConfig.h +104 -0
- package/ios/config/InputConfig.mm +664 -0
- package/ios/extensions/ColorExtension.h +7 -0
- package/ios/extensions/ColorExtension.mm +38 -0
- package/ios/extensions/FontExtension.h +11 -0
- package/ios/extensions/FontExtension.mm +72 -0
- package/ios/extensions/ImageExtension.h +34 -0
- package/ios/extensions/ImageExtension.mm +165 -0
- package/ios/extensions/LayoutManagerExtension.h +6 -0
- package/ios/extensions/LayoutManagerExtension.mm +443 -0
- package/ios/extensions/StringExtension.h +15 -0
- package/ios/extensions/StringExtension.mm +69 -0
- package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
- package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
- package/ios/generated/ReactNativeEnrichedSpec/Props.cpp +173 -0
- package/ios/generated/ReactNativeEnrichedSpec/Props.h +833 -0
- package/ios/generated/ReactNativeEnrichedSpec/RCTComponentViewHelpers.h +582 -0
- package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
- package/ios/generated/ReactNativeEnrichedSpec/States.cpp +16 -0
- package/ios/generated/ReactNativeEnrichedSpec/States.h +20 -0
- package/ios/inputParser/InputParser.h +11 -0
- package/ios/inputParser/InputParser.mm +1463 -0
- package/ios/inputTextView/InputTextView.h +6 -0
- package/ios/inputTextView/InputTextView.mm +285 -0
- package/ios/interfaces/AttributeEntry.h +9 -0
- package/ios/interfaces/AttributeEntry.mm +4 -0
- package/ios/interfaces/BaseStyleProtocol.h +17 -0
- package/ios/interfaces/ImageAttachment.h +11 -0
- package/ios/interfaces/ImageAttachment.mm +107 -0
- package/ios/interfaces/ImageData.h +10 -0
- package/ios/interfaces/ImageData.mm +4 -0
- package/ios/interfaces/LinkData.h +11 -0
- package/ios/interfaces/LinkData.mm +29 -0
- package/ios/interfaces/LinkRegexConfig.h +19 -0
- package/ios/interfaces/LinkRegexConfig.mm +37 -0
- package/ios/interfaces/MediaAttachment.h +23 -0
- package/ios/interfaces/MediaAttachment.mm +31 -0
- package/ios/interfaces/MentionParams.h +8 -0
- package/ios/interfaces/MentionParams.mm +4 -0
- package/ios/interfaces/MentionStyleProps.h +13 -0
- package/ios/interfaces/MentionStyleProps.mm +63 -0
- package/ios/interfaces/StyleBase.h +36 -0
- package/ios/interfaces/StyleBase.mm +256 -0
- package/ios/interfaces/StyleHeaders.h +102 -0
- package/ios/interfaces/StylePair.h +9 -0
- package/ios/interfaces/StylePair.mm +4 -0
- package/ios/interfaces/StyleTypeEnum.h +26 -0
- package/ios/interfaces/TextDecorationLineEnum.h +6 -0
- package/ios/interfaces/TextDecorationLineEnum.mm +4 -0
- package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +19 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.h +44 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +103 -0
- package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
- package/ios/internals/EnrichedTextInputViewState.h +22 -0
- package/ios/styles/BlockQuoteStyle.mm +55 -0
- package/ios/styles/BoldStyle.mm +37 -0
- package/ios/styles/CheckboxListStyle.mm +153 -0
- package/ios/styles/CodeBlockStyle.mm +49 -0
- package/ios/styles/H1Style.mm +20 -0
- package/ios/styles/H2Style.mm +20 -0
- package/ios/styles/H3Style.mm +20 -0
- package/ios/styles/H4Style.mm +20 -0
- package/ios/styles/H5Style.mm +20 -0
- package/ios/styles/H6Style.mm +20 -0
- package/ios/styles/HeadingStyleBase.mm +65 -0
- package/ios/styles/ImageStyle.mm +146 -0
- package/ios/styles/InlineCodeStyle.mm +65 -0
- package/ios/styles/ItalicStyle.mm +37 -0
- package/ios/styles/LinkStyle.mm +532 -0
- package/ios/styles/MentionStyle.mm +538 -0
- package/ios/styles/OrderedListStyle.mm +86 -0
- package/ios/styles/StrikethroughStyle.mm +25 -0
- package/ios/styles/UnderlineStyle.mm +24 -0
- package/ios/styles/UnorderedListStyle.mm +86 -0
- package/ios/utils/CheckboxHitTestUtils.h +10 -0
- package/ios/utils/CheckboxHitTestUtils.mm +122 -0
- package/ios/utils/DotReplacementUtils.h +10 -0
- package/ios/utils/DotReplacementUtils.mm +68 -0
- package/ios/utils/KeyboardUtils.h +7 -0
- package/ios/utils/KeyboardUtils.mm +31 -0
- package/ios/utils/OccurenceUtils.h +44 -0
- package/ios/utils/OccurenceUtils.mm +179 -0
- package/ios/utils/ParagraphAttributesUtils.h +15 -0
- package/ios/utils/ParagraphAttributesUtils.mm +257 -0
- package/ios/utils/RangeUtils.h +12 -0
- package/ios/utils/RangeUtils.mm +183 -0
- package/ios/utils/TextBlockTapGestureRecognizer.h +17 -0
- package/ios/utils/TextBlockTapGestureRecognizer.mm +56 -0
- package/ios/utils/TextInsertionUtils.h +17 -0
- package/ios/utils/TextInsertionUtils.mm +64 -0
- package/ios/utils/WordsUtils.h +7 -0
- package/ios/utils/WordsUtils.mm +98 -0
- package/ios/utils/ZeroWidthSpaceUtils.h +9 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +270 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/EnrichedTextInput.js +304 -0
- package/lib/module/native/EnrichedTextInput.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/spec/EnrichedTextInputNativeComponent.ts +517 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/EnrichedTextInputDefaultProps.js +12 -0
- package/lib/module/utils/EnrichedTextInputDefaultProps.js.map +1 -0
- package/lib/module/utils/normalizeHtmlStyle.js +155 -0
- package/lib/module/utils/normalizeHtmlStyle.js.map +1 -0
- package/lib/module/utils/nullthrows.js +9 -0
- package/lib/module/utils/nullthrows.js.map +1 -0
- package/lib/module/utils/regexParser.js +46 -0
- package/lib/module/utils/regexParser.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native/EnrichedTextInput.d.ts +3 -0
- package/lib/typescript/src/native/EnrichedTextInput.d.ts.map +1 -0
- package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts +397 -0
- package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +447 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts +10 -0
- package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts.map +1 -0
- package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts +4 -0
- package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -0
- package/lib/typescript/src/utils/nullthrows.d.ts +2 -0
- package/lib/typescript/src/utils/nullthrows.d.ts.map +1 -0
- package/lib/typescript/src/utils/regexParser.d.ts +3 -0
- package/lib/typescript/src/utils/regexParser.d.ts.map +1 -0
- package/package.json +226 -0
- package/react-native.config.js +13 -0
- package/src/index.tsx +20 -0
- package/src/native/EnrichedTextInput.tsx +370 -0
- package/src/spec/EnrichedTextInputNativeComponent.ts +517 -0
- package/src/types.ts +499 -0
- package/src/utils/EnrichedTextInputDefaultProps.ts +9 -0
- package/src/utils/normalizeHtmlStyle.ts +199 -0
- package/src/utils/nullthrows.ts +7 -0
- 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
|