├── .cargo └── config.toml ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── add-new-issues-to-verticals-project.yml │ ├── android.yml │ ├── deploy-web-demo.yml │ ├── ios.yml │ ├── publish.yml │ ├── react-build.yml │ ├── rust.yml │ ├── sonarqube.yml │ ├── tag-release.yml │ ├── triage-labelled.yml │ └── wasm.yml ├── .gitignore ├── .slather.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── bindings ├── wysiwyg-ffi │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── src │ │ ├── ffi_action_state.rs │ │ ├── ffi_composer_action.rs │ │ ├── ffi_composer_model.rs │ │ ├── ffi_composer_state.rs │ │ ├── ffi_composer_update.rs │ │ ├── ffi_dom_creation_error.rs │ │ ├── ffi_link_actions.rs │ │ ├── ffi_mention_detector.rs │ │ ├── ffi_mentions_state.rs │ │ ├── ffi_menu_action.rs │ │ ├── ffi_menu_state.rs │ │ ├── ffi_pattern_key.rs │ │ ├── ffi_suggestion_pattern.rs │ │ ├── ffi_text_update.rs │ │ ├── into_ffi.rs │ │ ├── lib.rs │ │ └── wysiwyg_composer.udl │ └── uniffi.toml └── wysiwyg-wasm │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ └── src │ └── lib.rs ├── build_xcframework.sh ├── crates ├── matrix_mentions │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── mention.rs └── wysiwyg │ ├── Cargo.toml │ ├── src │ ├── action_state.rs │ ├── char.rs │ ├── composer_action.rs │ ├── composer_model.rs │ ├── composer_model │ │ ├── base.rs │ │ ├── code_block.rs │ │ ├── delete_text.rs │ │ ├── example_format.rs │ │ ├── format.rs │ │ ├── format_inline_code.rs │ │ ├── hyperlinks.rs │ │ ├── lists.rs │ │ ├── mentions.rs │ │ ├── menu_action.rs │ │ ├── menu_state.rs │ │ ├── new_lines.rs │ │ ├── quotes.rs │ │ ├── replace_text.rs │ │ ├── selection.rs │ │ └── undo_redo.rs │ ├── composer_state.rs │ ├── composer_update.rs │ ├── dom.rs │ ├── dom │ │ ├── action_list.rs │ │ ├── dom_block_nodes.rs │ │ ├── dom_creation_error.rs │ │ ├── dom_handle.rs │ │ ├── dom_invariants.rs │ │ ├── dom_list_methods.rs │ │ ├── dom_methods.rs │ │ ├── dom_struct.rs │ │ ├── find_extended_range.rs │ │ ├── find_range.rs │ │ ├── find_result.rs │ │ ├── insert_node_at_cursor.rs │ │ ├── insert_parent.rs │ │ ├── iter.rs │ │ ├── join_nodes.rs │ │ ├── nodes.rs │ │ ├── nodes │ │ │ ├── container_node.rs │ │ │ ├── dom_node.rs │ │ │ ├── line_break_node.rs │ │ │ ├── mention_node.rs │ │ │ └── text_node.rs │ │ ├── parser.rs │ │ ├── parser │ │ │ ├── markdown.rs │ │ │ ├── markdown │ │ │ │ └── markdown_html_parser.rs │ │ │ ├── padom.rs │ │ │ ├── padom_creation_error.rs │ │ │ ├── padom_creator.rs │ │ │ ├── padom_handle.rs │ │ │ ├── padom_node.rs │ │ │ ├── panode_container.rs │ │ │ ├── panode_text.rs │ │ │ ├── paqual_name.rs │ │ │ └── parse.rs │ │ ├── range.rs │ │ ├── to_html.rs │ │ ├── to_markdown.rs │ │ ├── to_plain_text.rs │ │ ├── to_raw_text.rs │ │ ├── to_tree.rs │ │ └── unicode_string.rs │ ├── format_type.rs │ ├── lib.rs │ ├── link_action.rs │ ├── list_type.rs │ ├── location.rs │ ├── mentions_state.rs │ ├── menu_action.rs │ ├── menu_state.rs │ ├── pattern_key.rs │ ├── suggestion_pattern.rs │ ├── tests.rs │ ├── tests │ │ ├── test_characters.rs │ │ ├── test_deleting.rs │ │ ├── test_emoji_replacement.rs │ │ ├── test_formatting.rs │ │ ├── test_get_link_action.rs │ │ ├── test_links.rs │ │ ├── test_lists.rs │ │ ├── test_lists_with_blocks.rs │ │ ├── test_mentions.rs │ │ ├── test_menu_action.rs │ │ ├── test_menu_state.rs │ │ ├── test_paragraphs.rs │ │ ├── test_remove_links.rs │ │ ├── test_selection.rs │ │ ├── test_set_content.rs │ │ ├── test_suggestions.rs │ │ ├── test_to_markdown.rs │ │ ├── test_to_message_html.rs │ │ ├── test_to_plain_text.rs │ │ ├── test_to_raw_text.rs │ │ ├── test_to_tree.rs │ │ ├── test_undo_redo.rs │ │ ├── testutils_composer_model.rs │ │ ├── testutils_conversion.rs │ │ └── testutils_dom.rs │ └── text_update.rs │ └── tests │ └── tests.rs ├── package-lock.json ├── platforms ├── android │ ├── .gitignore │ ├── .idea │ │ ├── .gitignore │ │ ├── .name │ │ ├── compiler.xml │ │ ├── inspectionProfiles │ │ │ └── Project_Default.xml │ │ └── vcs.xml │ ├── README.md │ ├── build.gradle │ ├── coverage.gradle │ ├── example-compose │ │ ├── .gitignore │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── io │ │ │ │ └── element │ │ │ │ └── wysiwyg │ │ │ │ └── compose │ │ │ │ ├── ComposeApplication.kt │ │ │ │ ├── DefaultMentionDisplayHandler.kt │ │ │ │ ├── LinkDialog.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── SuggestionsView.kt │ │ │ │ ├── matrix │ │ │ │ ├── Mention.kt │ │ │ │ └── MentionType.kt │ │ │ │ └── ui │ │ │ │ ├── components │ │ │ │ └── FormattingButtons.kt │ │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ └── Theme.kt │ │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── ic_code.xml │ │ │ ├── ic_code_block.xml │ │ │ ├── ic_format_bold.xml │ │ │ ├── ic_format_italic.xml │ │ │ ├── ic_format_strikethrough.xml │ │ │ ├── ic_format_underline.xml │ │ │ ├── ic_indent.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_link.xml │ │ │ ├── ic_ordered_list.xml │ │ │ ├── ic_quote.xml │ │ │ ├── ic_redo.xml │ │ │ ├── ic_undo.xml │ │ │ ├── ic_unindent.xml │ │ │ └── ic_unordered_list.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ ├── example-view │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── io │ │ │ │ └── element │ │ │ │ └── android │ │ │ │ └── wysiwyg │ │ │ │ └── poc │ │ │ │ ├── ExampleApplication.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── RichTextEditor.kt │ │ │ │ └── matrix │ │ │ │ ├── MatrixMentionMentionDisplayHandler.kt │ │ │ │ ├── Mention.kt │ │ │ │ └── MentionType.kt │ │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── editor_menu_bg_selector.xml │ │ │ ├── editor_menu_text_selector.xml │ │ │ ├── ic_code.xml │ │ │ ├── ic_code_block.xml │ │ │ ├── ic_format_bold.xml │ │ │ ├── ic_format_italic.xml │ │ │ ├── ic_format_strikethrough.xml │ │ │ ├── ic_format_underline.xml │ │ │ ├── ic_indent.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_link.xml │ │ │ ├── ic_ordered_list.xml │ │ │ ├── ic_quote.xml │ │ │ ├── ic_redo.xml │ │ │ ├── ic_undo.xml │ │ │ ├── ic_unindent.xml │ │ │ └── ic_unordered_list.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── dialog_set_link.xml │ │ │ └── view_rich_text_editor.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ └── ic_launcher_round.webp │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── backup_rules.xml │ │ │ └── data_extraction_rules.xml │ ├── generate_coverage_report.sh │ ├── gradle.properties │ ├── gradle │ │ ├── libs.versions.toml │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── library-compose │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── consumer-rules.pro │ │ ├── gradle.properties │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── io │ │ │ │ └── element │ │ │ │ └── android │ │ │ │ └── wysiwyg │ │ │ │ └── compose │ │ │ │ ├── RichTextEditorActionsTest.kt │ │ │ │ ├── RichTextEditorStateTest.kt │ │ │ │ ├── RichTextEditorStyleTest.kt │ │ │ │ ├── RichTextEditorTest.kt │ │ │ │ └── testutils │ │ │ │ ├── ComposeTestRuleExt.kt │ │ │ │ ├── ComposerActions.kt │ │ │ │ ├── EditorActions.kt │ │ │ │ ├── StateFactory.kt │ │ │ │ └── ViewMatchers.kt │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── element │ │ │ │ │ └── android │ │ │ │ │ └── wysiwyg │ │ │ │ │ └── compose │ │ │ │ │ ├── EditorStyledText.kt │ │ │ │ │ ├── RichTextEditor.kt │ │ │ │ │ ├── RichTextEditorDefaults.kt │ │ │ │ │ ├── RichTextEditorState.kt │ │ │ │ │ ├── RichTextEditorStyle.kt │ │ │ │ │ ├── StyledHtmlConverter.kt │ │ │ │ │ └── internal │ │ │ │ │ ├── FakeViewConnection.kt │ │ │ │ │ ├── RichTextEditorStyleExt.kt │ │ │ │ │ └── ViewAction.kt │ │ │ └── res │ │ │ │ └── drawable │ │ │ │ └── cursor.xml │ │ │ └── test │ │ │ └── java │ │ │ └── io │ │ │ └── element │ │ │ └── android │ │ │ └── wysiwyg │ │ │ └── compose │ │ │ └── FakeRichTextEditorStateTest.kt │ ├── library │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── gradle.properties │ │ └── src │ │ │ ├── androidTest │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── element │ │ │ │ │ └── android │ │ │ │ │ └── wysiwyg │ │ │ │ │ ├── EditorEditTextInputTests.kt │ │ │ │ │ ├── EditorStyledTextViewTest.kt │ │ │ │ │ ├── fakes │ │ │ │ │ └── FakeStyleConfig.kt │ │ │ │ │ ├── inputhandlers │ │ │ │ │ └── InterceptInputConnectionIntegrationTest.kt │ │ │ │ │ └── test │ │ │ │ │ └── utils │ │ │ │ │ ├── AnyViewAction.kt │ │ │ │ │ ├── ClickActions.kt │ │ │ │ │ ├── EditorActions.kt │ │ │ │ │ ├── FakeLinkClickedListener.kt │ │ │ │ │ ├── ImeActions.kt │ │ │ │ │ ├── ScreenshotFailureHandler.kt │ │ │ │ │ ├── SpanUtils.kt │ │ │ │ │ ├── TestActivity.kt │ │ │ │ │ ├── TestMentionDisplayHandler.kt │ │ │ │ │ ├── TextInputMatchers.kt │ │ │ │ │ ├── TextViewActions.kt │ │ │ │ │ └── UriContentListener.kt │ │ │ └── res │ │ │ │ ├── layout │ │ │ │ └── activity_test.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ └── ids.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── element │ │ │ │ │ └── android │ │ │ │ │ └── wysiwyg │ │ │ │ │ ├── EditorEditText.kt │ │ │ │ │ ├── EditorStyledTextView.kt │ │ │ │ │ ├── EditorTextWatcher.kt │ │ │ │ │ ├── display │ │ │ │ │ ├── MentionDisplayHandler.kt │ │ │ │ │ └── TextDisplay.kt │ │ │ │ │ ├── extensions │ │ │ │ │ ├── ComposerExtensions.kt │ │ │ │ │ └── RustExtensions.kt │ │ │ │ │ ├── inputhandlers │ │ │ │ │ └── InterceptInputConnection.kt │ │ │ │ │ ├── internal │ │ │ │ │ ├── display │ │ │ │ │ │ └── MemoizedLinkDisplayHandler.kt │ │ │ │ │ ├── utils │ │ │ │ │ │ ├── AndroidHtmlConverter.kt │ │ │ │ │ │ ├── TextRangeHelper.kt │ │ │ │ │ │ └── UriContentListener.kt │ │ │ │ │ ├── view │ │ │ │ │ │ ├── EditorEditTextAttributeReader.kt │ │ │ │ │ │ ├── EditorStyledTextViewAttributeReader.kt │ │ │ │ │ │ ├── LayoutExtensions.kt │ │ │ │ │ │ ├── ViewLazyViewModelExtension.kt │ │ │ │ │ │ └── models │ │ │ │ │ │ │ └── LinkActionExt.kt │ │ │ │ │ └── viewmodel │ │ │ │ │ │ ├── EditorInputAction.kt │ │ │ │ │ │ ├── EditorViewModel.kt │ │ │ │ │ │ └── ReplaceTextResult.kt │ │ │ │ │ ├── utils │ │ │ │ │ ├── CharContants.kt │ │ │ │ │ ├── EditorIndexMapper.kt │ │ │ │ │ ├── HtmlConverter.kt │ │ │ │ │ ├── HtmlToSpansParser.kt │ │ │ │ │ ├── LoggingConfig.kt │ │ │ │ │ ├── ResourcesHelper.kt │ │ │ │ │ ├── RustCleanerTask.kt │ │ │ │ │ ├── RustErrorCollector.kt │ │ │ │ │ └── ThrowableExt.kt │ │ │ │ │ └── view │ │ │ │ │ ├── StyleConfig.kt │ │ │ │ │ ├── inlinebg │ │ │ │ │ ├── BlockRenderer.kt │ │ │ │ │ ├── SpanBackgroundHelper.kt │ │ │ │ │ ├── SpanBackgroundHelperFactory.kt │ │ │ │ │ └── SpanBackgroundRenderer.kt │ │ │ │ │ ├── models │ │ │ │ │ ├── InlineFormat.kt │ │ │ │ │ └── LinkAction.kt │ │ │ │ │ └── spans │ │ │ │ │ ├── BlockSpan.kt │ │ │ │ │ ├── CodeBlockSpan.kt │ │ │ │ │ ├── CodeSpanConstants.kt │ │ │ │ │ ├── CustomMentionSpan.kt │ │ │ │ │ ├── ExtraCharacterSpan.kt │ │ │ │ │ ├── InlineCodeSpan.kt │ │ │ │ │ ├── LinkSpan.kt │ │ │ │ │ ├── OrderedListSpan.kt │ │ │ │ │ ├── PillSpan.kt │ │ │ │ │ ├── PlainAtRoomMentionDisplaySpan.kt │ │ │ │ │ ├── QuoteSpan.kt │ │ │ │ │ ├── ReuseSourceSpannableFactory.kt │ │ │ │ │ └── UnorderedListSpan.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ ├── code_block_bg.xml │ │ │ │ ├── inline_code_multi_line_bg_left.xml │ │ │ │ ├── inline_code_multi_line_bg_mid.xml │ │ │ │ ├── inline_code_multi_line_bg_right.xml │ │ │ │ └── inline_code_single_line_bg.xml │ │ │ │ ├── values-night │ │ │ │ └── colors.xml │ │ │ │ └── values │ │ │ │ ├── attrs.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── integers.xml │ │ │ │ └── styles.xml │ │ │ └── test │ │ │ └── kotlin │ │ │ └── io │ │ │ └── element │ │ │ └── android │ │ │ └── wysiwyg │ │ │ ├── internal │ │ │ └── utils │ │ │ │ └── TextRangeHelperTest.kt │ │ │ ├── mocks │ │ │ ├── MockComposer.kt │ │ │ ├── MockComposerUpdateFactory.kt │ │ │ └── MockTextUpdateFactory.kt │ │ │ ├── test │ │ │ ├── fakes │ │ │ │ └── FakeStyleConfig.kt │ │ │ └── utils │ │ │ │ └── SpanUtils.kt │ │ │ ├── utils │ │ │ ├── AndroidHtmlConverterTest.kt │ │ │ ├── BasicHtmlConverter.kt │ │ │ ├── EditorIndexMapperTests.kt │ │ │ └── HtmlToSpansParserTest.kt │ │ │ └── viewmodel │ │ │ └── EditorViewModelTest.kt │ ├── plugins │ │ └── settings.gradle.kts │ ├── scripts │ │ └── ci_test.sh │ ├── settings.gradle │ └── test │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── element │ │ └── android │ │ └── wysiwyg │ │ └── test │ │ └── rules │ │ ├── DismissAnrRule.kt │ │ ├── FlakyEmulatorRule.kt │ │ └── RetryOnFailureRule.kt ├── ios │ ├── example │ │ ├── .gitignore │ │ ├── .swiftformat │ │ ├── .swiftlint.yml │ │ ├── Brewfile │ │ ├── Brewfile.lock.json │ │ ├── README.md │ │ ├── Shared │ │ │ ├── View+Accessibility.swift │ │ │ └── WysiwygSharedConstants.swift │ │ ├── Wysiwyg.xcodeproj │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ │ ├── IDETemplateMacros.plist │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── swiftpm │ │ │ │ │ └── Package.resolved │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── Wysiwyg.xcscheme │ │ ├── Wysiwyg │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Extensions │ │ │ │ ├── UIAlertController.swift │ │ │ │ ├── View.swift │ │ │ │ └── WysiwygAction+Utils.swift │ │ │ ├── Info.plist │ │ │ ├── Mocks │ │ │ │ ├── Commands.swift │ │ │ │ ├── Rooms.swift │ │ │ │ └── Users.swift │ │ │ ├── Pills │ │ │ │ ├── SerializationService.swift │ │ │ │ ├── WysiwygAttachmentView.swift │ │ │ │ ├── WysiwygAttachmentViewProvider.swift │ │ │ │ ├── WysiwygMentionReplacer.swift │ │ │ │ ├── WysiwygTextAttachment.swift │ │ │ │ └── WysiwygTextAttachmentData.swift │ │ │ ├── Preview Content │ │ │ │ └── Preview Assets.xcassets │ │ │ │ │ └── Contents.json │ │ │ ├── Views │ │ │ │ ├── AlertHelper.swift │ │ │ │ ├── Composer.swift │ │ │ │ ├── ComposerActionToolbar.swift │ │ │ │ ├── ContentView.swift │ │ │ │ └── WysiwygSuggestionList.swift │ │ │ └── WysiwygApp.swift │ │ ├── WysiwygUITests │ │ │ ├── WysiwygUITests+Autocorrection.swift │ │ │ ├── WysiwygUITests+CodeBlocks.swift │ │ │ ├── WysiwygUITests+Format.swift │ │ │ ├── WysiwygUITests+Indent.swift │ │ │ ├── WysiwygUITests+InlineCode.swift │ │ │ ├── WysiwygUITests+Keyboard.swift │ │ │ ├── WysiwygUITests+Links.swift │ │ │ ├── WysiwygUITests+Lists.swift │ │ │ ├── WysiwygUITests+PlainTextMode.swift │ │ │ ├── WysiwygUITests+Quotes.swift │ │ │ ├── WysiwygUITests+Suggestions.swift │ │ │ ├── WysiwygUITests+Typing.swift │ │ │ └── WysiwygUITests.swift │ │ ├── ios-test-coverage.sh │ │ └── ios-ui-test-coverage.sh │ ├── lib │ │ └── WysiwygComposer │ │ │ ├── .gitignore │ │ │ ├── .swiftformat │ │ │ ├── .swiftlint.yml │ │ │ ├── .swiftpm │ │ │ └── xcode │ │ │ │ ├── package.xcworkspace │ │ │ │ └── xcshareddata │ │ │ │ │ ├── IDETemplateMacros.plist │ │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ ├── WysiwygComposer.xcscheme │ │ │ │ └── WysiwygComposerTests.xcscheme │ │ │ ├── Package.resolved │ │ │ ├── Package.swift │ │ │ ├── SECURITY.md │ │ │ ├── Sources │ │ │ ├── DTCoreTextExtended │ │ │ │ ├── UIFont+AttributedStringBuilder.m │ │ │ │ └── include │ │ │ │ │ └── UIFont+AttributedStringBuilder.h │ │ │ ├── HTMLParser │ │ │ │ ├── BlockStyle.swift │ │ │ │ ├── BuildHTMLAttributedError.swift │ │ │ │ ├── Extensions │ │ │ │ │ ├── CGRect.swift │ │ │ │ │ ├── DTCoreText │ │ │ │ │ │ ├── DTHTMLElement.swift │ │ │ │ │ │ └── PlaceholderTextHTMLElement.swift │ │ │ │ │ ├── NSAttributedString+Attributes.swift │ │ │ │ │ ├── NSAttributedString+Range.swift │ │ │ │ │ ├── NSAttributedString.Key.swift │ │ │ │ │ ├── NSMutableAttributedString.swift │ │ │ │ │ ├── NSParagraphStyle.swift │ │ │ │ │ ├── NSRange.swift │ │ │ │ │ ├── String+Character.swift │ │ │ │ │ ├── UIColor.swift │ │ │ │ │ └── UITextView.swift │ │ │ │ ├── HTMLMentionReplacer.swift │ │ │ │ ├── HTMLParser.swift │ │ │ │ ├── HTMLParserHelpers.swift │ │ │ │ ├── HTMLParserStyle.swift │ │ │ │ ├── MentionContent.swift │ │ │ │ └── MentionReplacement.swift │ │ │ └── WysiwygComposer │ │ │ │ ├── Components │ │ │ │ ├── ComposerModelWrapper.swift │ │ │ │ ├── WysiwygComposerView │ │ │ │ │ ├── WysiwygComposerContent.swift │ │ │ │ │ ├── WysiwygComposerView.swift │ │ │ │ │ ├── WysiwygComposerViewModel.swift │ │ │ │ │ ├── WysiwygComposerViewModelProtocol.swift │ │ │ │ │ ├── WysiwygPillsFlusher.swift │ │ │ │ │ └── WysiwygTextView.swift │ │ │ │ ├── WysiwygKeyCommand.swift │ │ │ │ ├── WysiwygLinkOperation.swift │ │ │ │ └── WysiwygMentionType.swift │ │ │ │ ├── Extensions │ │ │ │ ├── CollectionDifference.swift │ │ │ │ ├── ComposerAction.swift │ │ │ │ ├── ComposerModel.swift │ │ │ │ ├── Logger.swift │ │ │ │ ├── NSRange.swift │ │ │ │ ├── PatternKey.swift │ │ │ │ ├── String.swift │ │ │ │ └── UITextView.swift │ │ │ │ └── Tools │ │ │ │ ├── MentionReplacer.swift │ │ │ │ └── StringDiffer.swift │ │ │ └── Tests │ │ │ ├── HTMLParserTests │ │ │ ├── Extensions │ │ │ │ ├── NSAttributedStringAttributesTests.swift │ │ │ │ ├── NSAttributedStringRangeTests.swift │ │ │ │ └── UIColorExtensionsTests.swift │ │ │ ├── HTMLParserTests+PermalinkReplacer.swift │ │ │ └── HTMLParserTests.swift │ │ │ ├── WysiwygComposerSnapshotTests │ │ │ ├── SnapshotTests+Blocks.swift │ │ │ ├── SnapshotTests+Common.swift │ │ │ ├── SnapshotTests+Links.swift │ │ │ ├── SnapshotTests+Lists.swift │ │ │ ├── SnapshotTests.swift │ │ │ └── __Snapshots__ │ │ │ │ ├── SnapshotTests+Blocks │ │ │ │ ├── testCodeBlockContent.1.png │ │ │ │ ├── testInlineCodeContent.1.png │ │ │ │ ├── testMultipleBlocksContent.1.png │ │ │ │ └── testQuoteContent.1.png │ │ │ │ ├── SnapshotTests+Common │ │ │ │ ├── testClearState.1.png │ │ │ │ └── testPlainTextContent.1.png │ │ │ │ ├── SnapshotTests+Links │ │ │ │ └── testLinkContent.1.png │ │ │ │ └── SnapshotTests+Lists │ │ │ │ ├── testIndentedListContent.1.png │ │ │ │ ├── testListInQuote.1.png │ │ │ │ ├── testMultipleListsContent.1.png │ │ │ │ ├── testOrderedListContent.1.png │ │ │ │ └── testUnorderedListContent.1.png │ │ │ └── WysiwygComposerTests │ │ │ ├── Components │ │ │ └── WysiwygComposerView │ │ │ │ ├── WysiwygComposerViewModelTests+Autocorrection.swift │ │ │ │ ├── WysiwygComposerViewModelTests+MentionsState.swift │ │ │ │ ├── WysiwygComposerViewModelTests+SetContent.swift │ │ │ │ ├── WysiwygComposerViewModelTests+Suggestions.swift │ │ │ │ └── WysiwygComposerViewModelTests.swift │ │ │ ├── Extensions │ │ │ ├── CollectionDifferenceTests.swift │ │ │ ├── ComposerModel.swift │ │ │ └── String+LatinLangugesTests.swift │ │ │ ├── TestConstants.swift │ │ │ ├── Tools │ │ │ └── StringDifferTests.swift │ │ │ ├── UITextViewTests.swift │ │ │ ├── WysiwygComposerTests+CodeBlocks.swift │ │ │ ├── WysiwygComposerTests+Emoji.swift │ │ │ ├── WysiwygComposerTests+Format.swift │ │ │ ├── WysiwygComposerTests+Indent.swift │ │ │ ├── WysiwygComposerTests+InlineCode.swift │ │ │ ├── WysiwygComposerTests+Links.swift │ │ │ ├── WysiwygComposerTests+Lists.swift │ │ │ ├── WysiwygComposerTests+Quotes.swift │ │ │ ├── WysiwygComposerTests+Suggestions.swift │ │ │ └── WysiwygComposerTests.swift │ └── tools │ │ └── release │ │ ├── .gitignore │ │ ├── Package.resolved │ │ ├── Package.swift │ │ └── Sources │ │ └── Release.swift └── web │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc.json │ ├── README.md │ ├── cypress.config.ts │ ├── cypress │ ├── e2e │ │ └── clipboard │ │ │ ├── cut.spec.ts │ │ │ └── paste.spec.ts │ └── support │ │ ├── commands.ts │ │ └── e2e.ts │ ├── example-wysiwyg │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts │ ├── index.html │ ├── lib │ ├── composer.test.ts │ ├── composer.ts │ ├── constants.ts │ ├── conversion.test.ts │ ├── conversion.ts │ ├── dom.test.ts │ ├── dom.ts │ ├── suggestion.test.tsx │ ├── suggestion.ts │ ├── testUtils │ │ ├── Editor.tsx │ │ └── selection.ts │ ├── types.ts │ ├── useComposerModel.test.tsx │ ├── useComposerModel.ts │ ├── useFormattingFunctions.ts │ ├── useListeners │ │ ├── assert.ts │ │ ├── event.test.ts │ │ ├── event.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── useListeners.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── useTestCases │ │ ├── assert.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── useTestCases.ts │ │ ├── utils.test.ts │ │ └── utils.ts │ ├── useWysiwyg.delete.test.tsx │ ├── useWysiwyg.formatting.test.tsx │ ├── useWysiwyg.inputEventProcessor.test.tsx │ ├── useWysiwyg.test.tsx │ ├── useWysiwyg.ts │ ├── useWysiwyg.undo-redo.test.tsx │ ├── utils.test.ts │ ├── utils.ts │ └── vite-env.d.ts │ ├── package.json │ ├── scripts │ ├── hack_exports.js │ └── hack_myfetch.js │ ├── src │ ├── App.tsx │ ├── images │ │ ├── bold.svg │ │ ├── code_block.svg │ │ ├── indent.svg │ │ ├── inline_code.svg │ │ ├── italic.svg │ │ ├── list_ordered.svg │ │ ├── list_unordered.svg │ │ ├── quote.svg │ │ ├── redo.svg │ │ ├── strike_through.svg │ │ ├── underline.svg │ │ ├── undo.svg │ │ └── unindent.svg │ ├── main.tsx │ └── vite-env.d.ts │ ├── test.setup.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── vite.demo.config.ts │ └── yarn.lock ├── renovate.json ├── rust-toolchain.toml ├── rustfmt.toml ├── sonar-project.properties ├── uniffi-bindgen ├── Cargo.toml └── src │ └── main.rs └── update_version.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | uniffi-bindgen = "run --package uniffi-bindgen --" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: T-Defect 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | ## Problem 16 | 17 | 18 | ## Steps to reproduce 19 | 25 | 26 | ## Related issues 27 | 28 | 29 | ## Proposed solution 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/add-new-issues-to-verticals-project.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to the verticals project board 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add issue to project 11 | runs-on: non-dind 12 | container: ubuntu 13 | steps: 14 | - uses: actions/add-to-project@main 15 | with: 16 | project-url: https://github.com/orgs/vector-im/projects/57 17 | github-token: ${{ secrets.RIOTROBOT_TOKEN_VERTICALS }} 18 | -------------------------------------------------------------------------------- /.github/workflows/tag-release.yml: -------------------------------------------------------------------------------- 1 | name: Tag Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | branches: 8 | - 'main' 9 | 10 | jobs: 11 | tag_release: 12 | if: ${{ github.event.pull_request.merged == true && startsWith( github.head_ref, 'version-' ) }} 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: 🧮 Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | token: ${{ secrets.PAT }} 19 | 20 | - name: Get Version 21 | uses: mad9000/actions-find-and-replace-string@5 22 | id: get-version 23 | with: 24 | source: ${{ github.head_ref }} 25 | find: 'version-' 26 | replace: '' 27 | 28 | - name: Add Tag 29 | uses: mathieudutour/github-tag-action@v6.2 30 | with: 31 | github_token: ${{ secrets.PAT }} 32 | custom_tag: ${{steps.get-version.outputs.value}} 33 | tag_prefix: '' -------------------------------------------------------------------------------- /.github/workflows/triage-labelled.yml: -------------------------------------------------------------------------------- 1 | name: Label 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | move_element_x_issues: 9 | name: Label new issues created with 'A-Rich-Text-Editor' for use on our board 10 | runs-on: ubuntu-latest 11 | # Skip in forks 12 | if: > 13 | github.repository == 'matrix-org/matrix-rich-text-editor' 14 | steps: 15 | - uses: actions/github-script@v7 16 | with: 17 | script: | 18 | github.rest.issues.addLabels({ 19 | issue_number: context.issue.number, 20 | owner: context.repo.owner, 21 | repo: context.repo.repo, 22 | labels: ['A-Rich-Text-Editor'] 23 | }) 24 | env: 25 | PROJECT_ID: "PVT_kwDOAM0swc4AHJKW" 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: Wasm 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | concurrency: 13 | group: ${{ github.ref == 'refs/heads/main' && format('wasm-build-main-{0}', github.sha) || format('wasm-build-pr-{0}', github.ref) }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set Version 24 | run: rustup default 1.76 25 | - name: Install `wasm-pack` 26 | run: cargo install wasm-pack 27 | - name: Test the `wysiwyg` crate 28 | working-directory: crates/wysiwyg 29 | run: wasm-pack test --release --firefox --headless -- --no-default-features --features js 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ENV 2 | **/.python-version 3 | **/.vscode 4 | **/.idea 5 | **/.DS_Store 6 | # Generated 7 | /target/ 8 | /bindings/wysiwyg-ffi/.cargo/ 9 | /.generated/ 10 | /bindings/wysiwyg-wasm/pkg/ 11 | /bindings/wysiwyg-wasm/node_modules/ 12 | /platforms/android/example/.idea/ 13 | /platforms/android/out/ 14 | /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/WysiwygComposer.swift 15 | /platforms/ios/lib/WysiwygComposer/WysiwygComposerFFI.xcframework 16 | /platforms/ios/lib/coverage/* 17 | /build/ 18 | .DS_Store 19 | 20 | # Swift release 21 | WysiwygComposerFFI.xcframework.zip 22 | matrix-rich-text-editor-swift 23 | -------------------------------------------------------------------------------- /.slather.yml: -------------------------------------------------------------------------------- 1 | coverage_service: cobertura_xml 2 | xcodeproj: platforms/ios/example/Wysiwyg.xcodeproj 3 | scheme: Wysiwyg 4 | output_directory: platforms/ios/lib/coverage 5 | build_directory: platforms/ios/example/DerivedData 6 | ignore: 7 | - platforms/ios/example/** 8 | - platforms/ios/lib/WysiwygComposer/Tests/** 9 | - platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/WysiwygComposer.swift -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "bindings/wysiwyg-ffi", 4 | "bindings/wysiwyg-wasm", 5 | "crates/wysiwyg", 6 | "crates/matrix_mentions", 7 | "uniffi-bindgen", 8 | ] 9 | default-members = [ 10 | "crates/wysiwyg", 11 | "crates/matrix_mentions", 12 | ] 13 | resolver = "2" 14 | 15 | [workspace.package] 16 | rust-version = "1.76" 17 | 18 | [workspace.dependencies] 19 | uniffi = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" } 20 | uniffi_macros = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" } 21 | uniffi_build = { git = "https://github.com/mozilla/uniffi-rs", rev = "789a9023b522562a95618443cee5a0d4f111c4c7" } 22 | 23 | [profile.release] 24 | opt-level = 'z' # Optimize for size. 25 | lto = true # Enable Link Time Optimization 26 | codegen-units = 1 # Reduce number of codegen units to increase optimizations. 27 | # Unwind on panic to allow error handling at the FFI boundary. Note this 28 | # imposes a small performance/size cost and it could be worth switching 29 | # the behaviour to 'abort' once the library is stable. 30 | panic = 'unwind' 31 | debug = true # Enable debug symbols. For example, we can use `dwarfdump` to check crash traces. 32 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Andy Balaam "] 3 | edition = "2021" 4 | homepage = "https://gitlab.com/andybalaam/wysiwyg-rust" 5 | repository = "https://gitlab.com/andybalaam/wysiwyg-rust" 6 | description = "Swift and Kotlin bindings for wysiwyg-rust" 7 | keywords = ["matrix", "chat", "messaging", "composer", "wysiwyg"] 8 | license = "Apache-2.0" 9 | name = "uniffi-wysiwyg-composer" 10 | version = "2.37.9" 11 | rust-version = { workspace = true } 12 | 13 | [features] 14 | default = [] 15 | assert-invariants = ["wysiwyg/assert-invariants"] 16 | 17 | [lib] 18 | crate-type = ["cdylib", "staticlib"] 19 | 20 | [dependencies] 21 | # Keep the uniffi version here in sync with the installed version of 22 | # uniffi-bindgen that is called from 23 | # ../../examples/example-android/app/build.gradle 24 | html-escape = "0.2.11" 25 | matrix_mentions = { path = "../../crates/matrix_mentions" } 26 | uniffi = { workspace = true } 27 | uniffi_macros = { workspace = true } 28 | widestring = "1.0.2" 29 | wysiwyg = { path = "../../crates/wysiwyg" } 30 | 31 | [build-dependencies] 32 | uniffi_build = { workspace = true, features = ["builtin-bindgen"] } 33 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | uniffi_build::generate_scaffolding("./src/wysiwyg_composer.udl") 3 | .expect("Building the UDL file failed"); 4 | } 5 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/ffi_action_state.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq, uniffi::Enum)] 2 | pub enum ActionState { 3 | Enabled, 4 | Reversed, 5 | Disabled, 6 | } 7 | 8 | impl From<&wysiwyg::ActionState> for ActionState { 9 | fn from(inner: &wysiwyg::ActionState) -> Self { 10 | match inner { 11 | wysiwyg::ActionState::Enabled => Self::Enabled, 12 | wysiwyg::ActionState::Reversed => Self::Reversed, 13 | wysiwyg::ActionState::Disabled => Self::Disabled, 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/ffi_composer_state.rs: -------------------------------------------------------------------------------- 1 | use widestring::Utf16String; 2 | use wysiwyg::ToHtml; 3 | 4 | #[derive(uniffi::Record)] 5 | pub struct ComposerState { 6 | pub html: Vec, 7 | pub start: u32, 8 | pub end: u32, 9 | } 10 | 11 | impl From> for ComposerState { 12 | fn from(state: wysiwyg::ComposerState) -> Self { 13 | let start_utf16_codeunit: usize = state.start.into(); 14 | let end_utf16_codeunit: usize = state.end.into(); 15 | Self { 16 | html: state.dom.to_html().into_vec(), 17 | start: u32::try_from(start_utf16_codeunit).unwrap(), 18 | end: u32::try_from(end_utf16_codeunit).unwrap(), 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/ffi_link_actions.rs: -------------------------------------------------------------------------------- 1 | use widestring::Utf16String; 2 | 3 | #[derive(uniffi::Enum)] 4 | pub enum LinkAction { 5 | CreateWithText, 6 | Create, 7 | Edit { url: String }, 8 | Disabled, 9 | } 10 | 11 | impl From> for LinkAction { 12 | fn from(inner: wysiwyg::LinkAction) -> Self { 13 | match inner { 14 | wysiwyg::LinkAction::CreateWithText => Self::CreateWithText, 15 | wysiwyg::LinkAction::Create => Self::Create, 16 | wysiwyg::LinkAction::Edit(url) => Self::Edit { 17 | url: url.to_string(), 18 | }, 19 | wysiwyg::LinkAction::Disabled => Self::Disabled, 20 | } 21 | } 22 | } 23 | 24 | #[derive(uniffi::Enum)] 25 | pub enum LinkActionUpdate { 26 | Keep, 27 | Update { link_action: LinkAction }, 28 | } 29 | 30 | impl From> for LinkActionUpdate { 31 | fn from(inner: wysiwyg::LinkActionUpdate) -> Self { 32 | match inner { 33 | wysiwyg::LinkActionUpdate::Keep => Self::Keep, 34 | wysiwyg::LinkActionUpdate::Update(action) => Self::Update { 35 | link_action: action.into(), 36 | }, 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/ffi_mention_detector.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | #[derive(Default, uniffi::Object)] 4 | pub struct MentionDetector {} 5 | 6 | impl MentionDetector { 7 | pub fn new() -> Self { 8 | Self {} 9 | } 10 | } 11 | 12 | #[uniffi::export] 13 | impl MentionDetector { 14 | pub fn is_mention(self: &Arc, url: String) -> bool { 15 | matrix_mentions::is_mention(&url) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/ffi_mentions_state.rs: -------------------------------------------------------------------------------- 1 | #[derive(uniffi::Record)] 2 | pub struct MentionsState { 3 | pub user_ids: Vec, 4 | pub room_ids: Vec, 5 | pub room_aliases: Vec, 6 | pub has_at_room_mention: bool, 7 | } 8 | 9 | impl From for MentionsState { 10 | fn from(value: wysiwyg::MentionsState) -> Self { 11 | Self { 12 | user_ids: value.user_ids.into_iter().collect(), 13 | room_ids: value.room_ids.into_iter().collect(), 14 | room_aliases: value.room_aliases.into_iter().collect(), 15 | has_at_room_mention: value.has_at_room_mention, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/ffi_menu_state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::into_ffi::IntoFfi; 4 | use crate::{ActionState, ComposerAction}; 5 | 6 | #[derive(Debug, PartialEq, Eq, uniffi::Enum)] 7 | pub enum MenuState { 8 | Keep, 9 | Update { 10 | action_states: HashMap, 11 | }, 12 | } 13 | 14 | impl MenuState { 15 | pub fn from(inner: wysiwyg::MenuState) -> Self { 16 | match inner { 17 | wysiwyg::MenuState::Keep => Self::Keep, 18 | wysiwyg::MenuState::Update(menu_update) => Self::Update { 19 | action_states: menu_update.action_states.into_ffi(), 20 | }, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/into_ffi.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ActionState, ComposerAction}; 4 | 5 | pub trait IntoFfi { 6 | fn into_ffi(self) -> HashMap; 7 | } 8 | 9 | impl IntoFfi for &HashMap { 10 | fn into_ffi(self) -> HashMap { 11 | self.iter().map(|(a, s)| (a.into(), s.into())).collect() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/src/wysiwyg_composer.udl: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | namespace wysiwyg_composer {}; -------------------------------------------------------------------------------- /bindings/wysiwyg-ffi/uniffi.toml: -------------------------------------------------------------------------------- 1 | [bindings.swift] 2 | module_name = "WysiwygComposer" 3 | -------------------------------------------------------------------------------- /bindings/wysiwyg-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Andy Balaam "] 3 | edition = "2021" 4 | homepage = "https://gitlab.com/andybalaam/wysiwyg-rust" 5 | repository = "https://gitlab.com/andybalaam/wysiwyg-rust" 6 | description = "WASM bindings for wysiwyg-rust" 7 | keywords = ["matrix", "chat", "messaging", "composer", "wysiwyg"] 8 | license = "Apache-2.0" 9 | name = "wysiwyg-wasm" 10 | version = "2.37.9" 11 | rust-version = { workspace = true } 12 | 13 | [package.metadata.wasm-pack.profile.profiling] 14 | wasm-opt = ['-O', '-g'] 15 | 16 | [package.metadata.wasm-pack.profile.profiling.wasm-bindgen] 17 | debug-js-glue = false 18 | demangle-name-section = true 19 | dwarf-debug-info = true 20 | 21 | [package.metadata.wasm-pack.profile.release] 22 | wasm-opt = ['-Oz'] 23 | 24 | [lib] 25 | crate-type = ["cdylib"] 26 | 27 | [dependencies] 28 | console_error_panic_hook = "0.1.7" 29 | html-escape = "0.2.11" 30 | js-sys = "0.3.60" 31 | wasm-bindgen = "0.2.83" 32 | wasm-bindgen-futures = "0.4.33" 33 | widestring = "1.0.2" 34 | wysiwyg = { path = "../../crates/wysiwyg", default-features = false, features = ["js"] } 35 | -------------------------------------------------------------------------------- /bindings/wysiwyg-wasm/README.md: -------------------------------------------------------------------------------- 1 | # `wysiwyg-wasm` 2 | 3 | WASM/JavaScript bindings for wysiwyg-rust. 4 | 5 | ## Building 6 | 7 | * [Install Rust](https://www.rust-lang.org/tools/install) 8 | * [Install NodeJS and NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 9 | * [Install wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) 10 | * Run: 11 | 12 | ```sh 13 | cd bindings/wysiwyg-wasm 14 | npm install 15 | npm run build 16 | #npm run test (no tests yet) 17 | ``` 18 | 19 | This will generate: 20 | 21 | ``` 22 | pkg/matrix_sdk_wysiwyg_bg.wasm 23 | pkg/matrix_sdk_wysiwyg_bg.wasm.d.ts 24 | pkg/matrix_sdk_wysiwyg.d.ts 25 | pkg/matrix_sdk_wysiwyg.js 26 | ... plus other files 27 | ``` 28 | 29 | These files should be copied into a web project and imported with code like: 30 | 31 | ```html 32 | 43 | ``` 44 | 45 | ## Profiling 46 | 47 | To generate a debugging/profiling Wasm module, use the following command 48 | instead of `npm run build`: 49 | 50 | ```sh 51 | $ npm run dev-build 52 | ``` -------------------------------------------------------------------------------- /bindings/wysiwyg-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wysiwyg-wasm", 3 | "version": "2.37.9", 4 | "homepage": "https://gitlab.com/andybalaam/wysiwyg-rust", 5 | "description": "WASM bindings for wysiwyg-rust", 6 | "license": "Apache-2.0", 7 | "collaborators": [ 8 | "Andy Balaam " 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://gitlab.com/andybalaam/wysiwyg-rust" 13 | }, 14 | "keywords": [ 15 | "matrix", 16 | "chat", 17 | "messaging", 18 | "wysiwyg" 19 | ], 20 | "main": "wysiwyg.js", 21 | "types": "pkg/wysiwyg.d.ts", 22 | "files": [ 23 | "pkg/wysiwyg_bg.wasm", 24 | "pkg/wysiwyg_bg.wasm.d.ts", 25 | "pkg/wysiwyg.js", 26 | "pkg/wysiwyg.d.ts" 27 | ], 28 | "devDependencies": { 29 | "wasm-pack": "^0.13.0", 30 | "jest": "^28.1.0", 31 | "typedoc": "^0.26.0" 32 | }, 33 | "engines": { 34 | "node": ">= 10" 35 | }, 36 | "scripts": { 37 | "dev-build": "WASM_BINDGEN_WEAKREF=1 wasm-pack build --profiling --target web --out-name wysiwyg --out-dir ./pkg", 38 | "build": "RUSTFLAGS='-C opt-level=s' WASM_BINDGEN_WEAKREF=1 wasm-pack build --release --target web --out-name wysiwyg --out-dir ./pkg", 39 | "test": "jest --verbose", 40 | "doc": "typedoc --tsconfig ." 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/matrix_mentions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Jonny Andrew "] 3 | homepage = "https://github.com/matrix-org/matrix-rich-text-editor" 4 | repository = "https://github.com/matrix-org/matrix-rich-text-editor" 5 | description = "Utilities for Matrix mentions" 6 | keywords = ["matrix", "chat", "messaging"] 7 | license = "Apache-2.0" 8 | name = "matrix_mentions" 9 | version = "0.1.0" 10 | edition = "2021" 11 | rust-version = { workspace = true } 12 | 13 | [features] 14 | default = ["custom-matrix-urls"] 15 | custom-matrix-urls = [] 16 | 17 | [dependencies] 18 | cfg-if = "1.0.0" 19 | ruma-common = "0.13.0" 20 | -------------------------------------------------------------------------------- /crates/matrix_mentions/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod mention; 16 | 17 | pub use crate::mention::{Mention, MentionKind, RoomIdentificationType}; 18 | 19 | pub fn is_mention(url: &str) -> bool { 20 | Mention::from_uri(url).is_some() 21 | } 22 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/action_state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use strum_macros::AsRefStr; 16 | 17 | #[derive(AsRefStr, Clone, Debug, PartialEq, Eq)] 18 | pub enum ActionState { 19 | Enabled, 20 | Reversed, 21 | Disabled, 22 | } 23 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/char.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub trait CharExt: Sized { 16 | fn nbsp() -> Self; 17 | } 18 | 19 | impl CharExt for char { 20 | fn nbsp() -> Self { 21 | '\u{A0}' 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/composer_action.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use strum_macros::{AsRefStr, EnumIter}; 16 | 17 | #[derive(AsRefStr, Debug, Clone, EnumIter, Eq, Hash, PartialEq)] 18 | pub enum ComposerAction { 19 | Bold, 20 | Italic, 21 | StrikeThrough, 22 | Underline, 23 | InlineCode, 24 | Link, 25 | Undo, 26 | Redo, 27 | OrderedList, 28 | UnorderedList, 29 | Indent, 30 | Unindent, 31 | CodeBlock, 32 | Quote, 33 | } 34 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/composer_model.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod base; 16 | pub mod code_block; 17 | pub mod delete_text; 18 | pub mod example_format; 19 | pub mod format; 20 | mod format_inline_code; 21 | pub mod hyperlinks; 22 | pub mod lists; 23 | pub mod mentions; 24 | pub mod menu_action; 25 | pub mod menu_state; 26 | pub mod new_lines; 27 | pub mod quotes; 28 | pub mod replace_text; 29 | pub mod selection; 30 | pub mod undo_redo; 31 | 32 | pub use base::ComposerModel; 33 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/find_result.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::dom::range::DomLocation; 16 | 17 | #[derive(Clone, Debug, PartialEq, Eq)] 18 | pub enum FindResult { 19 | Found(Vec), 20 | NotFound, 21 | } 22 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/nodes.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod container_node; 16 | pub mod dom_node; 17 | pub mod line_break_node; 18 | pub mod mention_node; 19 | pub mod text_node; 20 | 21 | pub use container_node::ContainerNode; 22 | pub use container_node::ContainerNodeKind; 23 | pub use dom_node::DomNode; 24 | pub use line_break_node::LineBreakNode; 25 | pub use mention_node::MentionNode; 26 | pub use mention_node::MentionNodeKind; 27 | pub use text_node::TextNode; 28 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/parser/markdown.rs: -------------------------------------------------------------------------------- 1 | pub mod markdown_html_parser; 2 | 3 | #[allow(unused_imports)] 4 | pub use markdown_html_parser::MarkdownHTMLParser; 5 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/parser/padom_creation_error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use super::PaDom; 16 | 17 | #[derive(Debug, PartialEq)] 18 | pub(crate) struct PaDomCreationError { 19 | pub(crate) dom: PaDom, 20 | pub(crate) parse_errors: Vec, 21 | } 22 | 23 | impl PaDomCreationError { 24 | pub(crate) fn new() -> Self { 25 | Self { 26 | dom: PaDom::new(), 27 | parse_errors: Vec::new(), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/parser/padom_handle.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[derive(Clone, Debug, PartialEq, Eq)] 16 | pub struct PaDomHandle(pub usize); 17 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/parser/panode_container.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use html5ever::QualName; 16 | 17 | use super::PaDomHandle; 18 | 19 | #[derive(Clone, Debug, PartialEq)] 20 | pub(crate) struct PaNodeContainer { 21 | pub(crate) name: QualName, 22 | pub(crate) attrs: Vec<(String, String)>, 23 | pub(crate) children: Vec, 24 | } 25 | impl PaNodeContainer { 26 | pub(crate) fn get_attr(&self, name: &str) -> Option<&str> { 27 | self.attrs 28 | .iter() 29 | .find(|(n, _v)| n == name) 30 | .map(|(_n, v)| v.as_str()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/parser/panode_text.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[derive(Clone, Debug, PartialEq)] 16 | pub(crate) struct PaNodeText { 17 | pub(crate) content: String, 18 | } 19 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/parser/paqual_name.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use html5ever::{LocalName, Namespace, QualName}; 16 | 17 | pub fn paqual_name(local_name: &str) -> QualName { 18 | QualName::new( 19 | None, 20 | Namespace::from("http://www.w3.org/1999/xhtml"), 21 | LocalName::from(local_name), 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/to_plain_text.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use super::UnicodeString; 16 | 17 | pub trait ToPlainText 18 | where 19 | S: UnicodeString, 20 | { 21 | fn to_plain_text(&self) -> S; 22 | } 23 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/dom/to_raw_text.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use super::UnicodeString; 16 | 17 | pub trait ToRawText 18 | where 19 | S: UnicodeString, 20 | { 21 | fn to_raw_text(&self) -> S; 22 | } 23 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/link_action.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::UnicodeString; 16 | 17 | #[derive(Clone, Debug, PartialEq, Eq)] 18 | pub enum LinkActionUpdate { 19 | Keep, 20 | Update(LinkAction), 21 | } 22 | 23 | #[derive(Clone, Debug, PartialEq, Eq)] 24 | pub enum LinkAction { 25 | CreateWithText, 26 | Create, 27 | Edit(S), 28 | Disabled, 29 | } 30 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/mentions_state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::collections::HashSet; 16 | 17 | #[derive(Default, Debug, PartialEq, Eq)] 18 | pub struct MentionsState { 19 | pub user_ids: HashSet, 20 | pub room_ids: HashSet, 21 | pub room_aliases: HashSet, 22 | pub has_at_room_mention: bool, 23 | } 24 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/menu_action.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::SuggestionPattern; 16 | 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | pub enum MenuAction { 19 | Keep, 20 | None, 21 | Suggestion(SuggestionPattern), 22 | } 23 | 24 | #[derive(Debug, Clone, PartialEq, Eq)] 25 | pub struct MenuActionSuggestion { 26 | pub suggestion_pattern: SuggestionPattern, 27 | } 28 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/menu_state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::action_state::ActionState; 16 | use crate::ComposerAction; 17 | use std::collections::HashMap; 18 | 19 | #[derive(Debug, Clone, PartialEq, Eq)] 20 | pub enum MenuState { 21 | Keep, 22 | Update(MenuStateUpdate), 23 | } 24 | 25 | #[derive(Debug, Clone, PartialEq, Eq)] 26 | pub struct MenuStateUpdate { 27 | pub action_states: HashMap, 28 | } 29 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/suggestion_pattern.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::PatternKey; 16 | 17 | #[derive(Clone, Debug, PartialEq, Eq)] 18 | pub struct SuggestionPattern { 19 | pub key: PatternKey, 20 | pub text: String, 21 | pub start: usize, 22 | pub end: usize, 23 | } 24 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/tests/test_suggestions.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::MenuAction; 16 | 17 | use super::testutils_composer_model::{cm, tx}; 18 | 19 | #[test] 20 | fn test_replace_text_suggestion() { 21 | let mut model = cm("|"); 22 | let update = model.replace_text("/".into()); 23 | let MenuAction::Suggestion(suggestion) = update.menu_action else { 24 | panic!("No suggestion pattern found") 25 | }; 26 | model.replace_text_suggestion("/invite".into(), suggestion, true); 27 | assert_eq!(tx(&model), "/invite |"); 28 | } 29 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/tests/testutils_conversion.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use widestring::Utf16String; 16 | 17 | pub fn utf16(s: &str) -> Utf16String { 18 | Utf16String::from_str(s) 19 | } 20 | -------------------------------------------------------------------------------- /crates/wysiwyg/src/text_update.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Matrix.org Foundation C.I.C. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::{dom::UnicodeString, Location}; 16 | 17 | #[derive(Debug, Clone, PartialEq, Eq)] 18 | pub enum TextUpdate 19 | where 20 | S: UnicodeString, 21 | { 22 | Keep, 23 | ReplaceAll(ReplaceAll), 24 | Select(Selection), 25 | } 26 | 27 | #[derive(Debug, Clone, PartialEq, Eq)] 28 | pub struct ReplaceAll 29 | where 30 | S: UnicodeString, 31 | { 32 | pub replacement_html: S, 33 | pub start: Location, 34 | pub end: Location, 35 | } 36 | 37 | #[derive(Debug, Clone, PartialEq, Eq)] 38 | pub struct Selection { 39 | pub start: Location, 40 | pub end: Location, 41 | } 42 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrix-rich-text-editor", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "matrix-rich-text-editor" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /platforms/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/deploymentTargetDropDown.xml 6 | /.idea/gradle.xml 7 | /.idea/libraries 8 | /.idea/misc.xml 9 | /.idea/modules.xml 10 | /.idea/workspace.xml 11 | /.idea/navEditor.xml 12 | /.idea/assetWizardSettings.xml 13 | /.idea/kotlinc.xml 14 | /.idea/androidTestResultsUserPreferences.xml 15 | .DS_Store 16 | **/build 17 | /captures 18 | .externalNativeBuild 19 | .cxx 20 | local.properties 21 | -------------------------------------------------------------------------------- /platforms/android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /platforms/android/.idea/.name: -------------------------------------------------------------------------------- 1 | Rich Text Editor -------------------------------------------------------------------------------- /platforms/android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/java/io/element/wysiwyg/compose/ComposeApplication.kt: -------------------------------------------------------------------------------- 1 | package io.element.wysiwyg.compose 2 | 3 | import android.app.Application 4 | import timber.log.Timber 5 | 6 | class ComposeApplication: Application() { 7 | override fun onCreate() { 8 | super.onCreate() 9 | Timber.plant(Timber.DebugTree()) 10 | } 11 | } -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/java/io/element/wysiwyg/compose/DefaultMentionDisplayHandler.kt: -------------------------------------------------------------------------------- 1 | package io.element.wysiwyg.compose 2 | 3 | import io.element.android.wysiwyg.display.MentionDisplayHandler 4 | import io.element.android.wysiwyg.display.TextDisplay 5 | 6 | class DefaultMentionDisplayHandler : MentionDisplayHandler { 7 | 8 | override fun resolveMentionDisplay( 9 | text: String, url: String 10 | ): TextDisplay { 11 | return TextDisplay.Pill 12 | } 13 | 14 | override fun resolveAtRoomMentionDisplay(): TextDisplay { 15 | return TextDisplay.Pill 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/java/io/element/wysiwyg/compose/matrix/Mention.kt: -------------------------------------------------------------------------------- 1 | package io.element.wysiwyg.compose.matrix 2 | 3 | /** 4 | * Utility model class for the sample app to represent a mention to a 5 | * matrix.org user or room 6 | */ 7 | sealed class Mention( 8 | val display: String, 9 | ) { 10 | abstract val key: String 11 | val link get() = "https://matrix.to/#/$key$display:matrix.org" 12 | val text get() = "$key$display" 13 | 14 | class Room( 15 | display: String 16 | ): Mention(display) { 17 | override val key: String = "#" 18 | } 19 | 20 | class User( 21 | display: String 22 | ): Mention(display) { 23 | override val key: String = "@" 24 | } 25 | 26 | class SlashCommand( 27 | display: String 28 | ): Mention(display) { 29 | override val key: String = "/" 30 | } 31 | 32 | object NotifyEveryone: Mention("room") { 33 | override val key: String = "@" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/java/io/element/wysiwyg/compose/matrix/MentionType.kt: -------------------------------------------------------------------------------- 1 | package io.element.wysiwyg.compose.matrix 2 | 3 | enum class MentionType { 4 | User, Room 5 | } 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/java/io/element/wysiwyg/compose/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package io.element.wysiwyg.compose.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_code.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_code_block.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_format_bold.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_format_italic.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_format_strikethrough.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_format_underline.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_indent.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_link.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_ordered_list.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_quote.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_redo.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_undo.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_unindent.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/drawable/ic_unordered_list.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/example-compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Compose Example 3 | Remove link 4 | Set link 5 | Insert link 6 | Text 7 | Link 8 | -------------------------------------------------------------------------------- /platforms/android/example-compose/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /platforms/android/example-view/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /platforms/android/example-view/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /platforms/android/generate_coverage_report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=$(dirname -- "$0") 4 | cd $DIR 5 | ./gradlew unitTestsWithCoverage 6 | ./gradlew instrumentationTestsWithCoverage 7 | ./gradlew generateCoverageReport 8 | open build/reports/jacoco/generateCoverageReport/html/index.html 9 | -------------------------------------------------------------------------------- /platforms/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /platforms/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /platforms/android/library-compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /platforms/android/library-compose/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/android/library-compose/consumer-rules.pro -------------------------------------------------------------------------------- /platforms/android/library-compose/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=wysiwyg-compose -------------------------------------------------------------------------------- /platforms/android/library-compose/src/androidTest/java/io/element/android/wysiwyg/compose/testutils/ComposeTestRuleExt.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.compose.testutils 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.test.junit4.ComposeContentTestRule 9 | import io.element.android.wysiwyg.compose.RichTextEditor 10 | import io.element.android.wysiwyg.compose.RichTextEditorState 11 | import io.element.android.wysiwyg.display.TextDisplay 12 | 13 | fun ComposeContentTestRule.showContent( 14 | state: RichTextEditorState, 15 | ) = setContent { 16 | MaterialTheme { 17 | RichTextEditor( 18 | state = state, modifier = Modifier.fillMaxWidth().background(Color.Cyan) 19 | ) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /platforms/android/library-compose/src/androidTest/java/io/element/android/wysiwyg/compose/testutils/EditorActions.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.compose.testutils 2 | 3 | import android.view.View 4 | import androidx.appcompat.widget.AppCompatEditText 5 | import androidx.test.espresso.UiController 6 | import androidx.test.espresso.ViewAction 7 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 8 | import org.hamcrest.Matcher 9 | 10 | object Editor { 11 | class SetSelection( 12 | private val start: Int, 13 | private val end: Int, 14 | ) : ViewAction { 15 | override fun getConstraints(): Matcher = isDisplayed() 16 | 17 | override fun getDescription(): String = "Set selection to $start, $end" 18 | 19 | override fun perform(uiController: UiController?, view: View?) { 20 | val editor = view as? AppCompatEditText ?: return 21 | editor.setSelection(start, end) 22 | } 23 | } 24 | 25 | } 26 | 27 | object EditorActions { 28 | fun setSelection(start: Int, end: Int) = Editor.SetSelection(start, end) 29 | } 30 | -------------------------------------------------------------------------------- /platforms/android/library-compose/src/androidTest/java/io/element/android/wysiwyg/compose/testutils/StateFactory.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.compose.testutils 2 | 3 | import io.element.android.wysiwyg.compose.RichTextEditorState 4 | 5 | object StateFactory { 6 | fun createState(): RichTextEditorState = RichTextEditorState() 7 | } -------------------------------------------------------------------------------- /platforms/android/library-compose/src/androidTest/java/io/element/android/wysiwyg/compose/testutils/ViewMatchers.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.compose.testutils 2 | 3 | import android.view.View 4 | import androidx.test.espresso.matcher.ViewMatchers 5 | import io.element.android.wysiwyg.EditorEditText 6 | import org.hamcrest.Matcher 7 | 8 | object ViewMatchers { 9 | fun isRichTextEditor(): Matcher = 10 | ViewMatchers.isAssignableFrom(EditorEditText::class.java) 11 | } -------------------------------------------------------------------------------- /platforms/android/library-compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /platforms/android/library-compose/src/main/java/io/element/android/wysiwyg/compose/internal/ViewAction.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.compose.internal 2 | 3 | import io.element.android.wysiwyg.view.models.InlineFormat 4 | 5 | internal sealed class ViewAction { 6 | data class ToggleInlineFormat(val inlineFormat: InlineFormat): ViewAction() 7 | data class ToggleList(val ordered: Boolean): ViewAction() 8 | data object ToggleCodeBlock: ViewAction() 9 | data object ToggleQuote: ViewAction() 10 | data object Undo: ViewAction() 11 | data object Redo: ViewAction() 12 | data object Indent: ViewAction() 13 | data object Unindent: ViewAction() 14 | data class SetHtml(val html: String): ViewAction() 15 | data class SetMarkdown(val markdown: String): ViewAction() 16 | data object RequestFocus: ViewAction() 17 | data class SetLink(val url: String?): ViewAction() 18 | data object RemoveLink: ViewAction() 19 | data class InsertLink(val url: String, val text: String): ViewAction() 20 | data class ReplaceSuggestionText(val text: String): ViewAction() 21 | data class InsertMentionAtSuggestion(val text: String, val url: String): ViewAction() 22 | data object InsertAtRoomMentionAtSuggestion: ViewAction() 23 | data class SetSelection(val start: Int, val end: Int): ViewAction() 24 | } 25 | -------------------------------------------------------------------------------- /platforms/android/library-compose/src/main/res/drawable/cursor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/android/library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /platforms/android/library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=wysiwyg -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/AnyViewAction.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import android.view.View 4 | import androidx.test.espresso.UiController 5 | import androidx.test.espresso.ViewAction 6 | import androidx.test.espresso.matcher.ViewMatchers 7 | import org.hamcrest.Matcher 8 | 9 | class AnyViewAction( 10 | private val description: String = "Custom view action", 11 | private val action: (View) -> Unit, 12 | ) : ViewAction { 13 | override fun getConstraints(): Matcher { 14 | return ViewMatchers.isDisplayed() 15 | } 16 | 17 | override fun getDescription(): String = description 18 | 19 | override fun perform(uiController: UiController, view: View) { 20 | action(view) 21 | uiController.loopMainThreadUntilIdle() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/ClickActions.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import android.view.InputDevice 4 | import android.view.MotionEvent 5 | import androidx.test.espresso.action.GeneralClickAction 6 | import androidx.test.espresso.action.Press 7 | import androidx.test.espresso.action.Tap 8 | 9 | fun clickXY(x: Float, y: Float): GeneralClickAction { 10 | return GeneralClickAction( 11 | Tap.SINGLE, 12 | { view -> 13 | val screenPos = IntArray(2) 14 | view.getLocationOnScreen(screenPos) 15 | val screenX = screenPos[0] + x 16 | val screenY = screenPos[1] + y 17 | floatArrayOf(screenX, screenY) 18 | }, 19 | Press.FINGER, 20 | InputDevice.SOURCE_MOUSE, 21 | MotionEvent.BUTTON_PRIMARY, 22 | ) 23 | } -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/FakeLinkClickedListener.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import org.junit.Assert 4 | 5 | class FakeLinkClickedListener: (String) -> Unit { 6 | private val clickedLinks: MutableList = mutableListOf() 7 | 8 | override fun invoke(link: String) { 9 | clickedLinks.add(link) 10 | } 11 | 12 | fun assertLinkClicked(url: String) { 13 | Assert.assertTrue(clickedLinks.size == 1) 14 | Assert.assertTrue(clickedLinks.contains(url)) 15 | } 16 | } -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/SpanUtils.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import android.text.TextUtils 4 | 5 | fun CharSequence.dumpSpans(): List { 6 | val spans = mutableListOf() 7 | TextUtils.dumpSpans( 8 | this, { span -> 9 | val spanWithoutHash = span.split(" ").filterIndexed { index, _ -> 10 | index != 1 11 | }.joinToString(" ") 12 | 13 | spans.add(spanWithoutHash) 14 | }, "" 15 | ) 16 | return spans 17 | } -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/TestActivity.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import io.element.android.wysiwyg.test.R 5 | 6 | class TestActivity : AppCompatActivity(R.layout.activity_test) 7 | -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/TestMentionDisplayHandler.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import io.element.android.wysiwyg.display.MentionDisplayHandler 4 | import io.element.android.wysiwyg.display.TextDisplay 5 | 6 | class TestMentionDisplayHandler( 7 | val textDisplay: TextDisplay = TextDisplay.Pill, 8 | ) : MentionDisplayHandler { 9 | override fun resolveAtRoomMentionDisplay(): TextDisplay = textDisplay 10 | override fun resolveMentionDisplay(text: String, url: String): TextDisplay = textDisplay 11 | } 12 | -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/java/io/element/android/wysiwyg/test/utils/UriContentListener.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import android.content.ClipData 4 | import android.net.Uri 5 | import android.view.View 6 | import androidx.core.view.ContentInfoCompat 7 | import androidx.core.view.OnReceiveContentListener 8 | 9 | class UriContentListener( 10 | private val onContent: (uri: Uri) -> Unit 11 | ) : OnReceiveContentListener { 12 | override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { 13 | val split = payload.partition { item -> item.uri != null } 14 | val uriContent = split.first 15 | val remaining = split.second 16 | 17 | if (uriContent != null) { 18 | val clip: ClipData = uriContent.clip 19 | for (i in 0 until clip.itemCount) { 20 | val uri = clip.getItemAt(i).uri 21 | // ... app-specific logic to handle the URI ... 22 | onContent(uri) 23 | } 24 | } 25 | // Return anything that we didn't handle ourselves. This preserves the default platform 26 | // behavior for text and anything else for which we are not implementing custom handling. 27 | return remaining 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #336699 4 | -------------------------------------------------------------------------------- /platforms/android/library/src/androidTest/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/display/MentionDisplayHandler.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.display 2 | 3 | /** 4 | * Clients can implement a mention display handler to let the editor 5 | * know how to display mentions. 6 | */ 7 | interface MentionDisplayHandler { 8 | /** 9 | * Return the method with which to display a given mention 10 | */ 11 | fun resolveMentionDisplay(text: String, url: String): TextDisplay 12 | 13 | /** 14 | * Return the method with which to display an at-room mention 15 | */ 16 | fun resolveAtRoomMentionDisplay(): TextDisplay 17 | } 18 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/display/TextDisplay.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.display 2 | 3 | import android.text.style.ReplacementSpan 4 | 5 | /** 6 | * Different ways to display text 7 | */ 8 | sealed class TextDisplay { 9 | /** 10 | * Display the text using a custom span 11 | */ 12 | data class Custom(val customSpan: ReplacementSpan): TextDisplay() 13 | 14 | /** 15 | * Display the text using a default pill shape 16 | */ 17 | object Pill: TextDisplay() 18 | 19 | /** 20 | * Display the text without any replacement 21 | */ 22 | object Plain: TextDisplay() 23 | } 24 | 25 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/extensions/ComposerExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.extensions 2 | 3 | import io.element.android.wysiwyg.BuildConfig 4 | import timber.log.Timber 5 | import uniffi.wysiwyg_composer.ComposerModelInterface 6 | import uniffi.wysiwyg_composer.ComposerState 7 | 8 | val LOG_ENABLED = BuildConfig.DEBUG 9 | 10 | /** 11 | * Get the current HTML representation of the formatted text in the Rust code, along with its 12 | * selection. 13 | */ 14 | fun ComposerState.dump() = "'${html.string()}' | Start: $start | End: $end" 15 | 16 | /** 17 | * Log the current state of the editor in the Rust code. 18 | */ 19 | fun ComposerModelInterface.log() = if (LOG_ENABLED) { 20 | Timber.d(toExampleFormat()) 21 | } else Unit 22 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/extensions/RustExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.extensions 2 | 3 | /** 4 | * Translates the Rust [UShort] list returned for strings into actual JVM Strings that we can use. 5 | */ 6 | internal fun List.string() = with(StringBuffer()) { 7 | this@string.forEach { 8 | appendCodePoint(it.toInt()) 9 | } 10 | toString() 11 | } 12 | 13 | /** 14 | * Translates a JVM String into a Rust [UShort] list. 15 | */ 16 | internal fun String.toUShortList(): List = encodeToByteArray() 17 | .map(Byte::toUShort) 18 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/internal/utils/AndroidHtmlConverter.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.internal.utils 2 | 3 | import io.element.android.wysiwyg.utils.HtmlConverter 4 | import io.element.android.wysiwyg.utils.HtmlToSpansParser 5 | 6 | internal class AndroidHtmlConverter( 7 | private val provideHtmlToSpansParser: (html: String) -> HtmlToSpansParser 8 | ) : HtmlConverter { 9 | 10 | override fun fromHtmlToSpans(html: String): CharSequence = 11 | provideHtmlToSpansParser(html).convert() 12 | 13 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/internal/utils/TextRangeHelper.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.internal.utils 2 | 3 | import android.text.Spanned 4 | import android.text.style.ReplacementSpan 5 | import kotlin.math.max 6 | import kotlin.math.min 7 | 8 | internal object TextRangeHelper { 9 | /** 10 | * Return a new range that covers the given range and extends it to cover 11 | * any replacement spans at either end. 12 | * 13 | * The range is returned as a pair of integers where the first is less than the last 14 | */ 15 | fun extendRangeToReplacementSpans( 16 | spanned: Spanned, 17 | start: Int, 18 | length: Int, 19 | ): Pair { 20 | require(length > 0) 21 | val end = start + length 22 | val spans = spanned.getSpans(start, end, ReplacementSpan::class.java) 23 | val firstReplacementSpanStart = spans.minOfOrNull { spanned.getSpanStart(it) } 24 | val lastReplacementSpanEnd = spans.maxOfOrNull { spanned.getSpanEnd(it) } 25 | val newStart = min(start, firstReplacementSpanStart ?: end) 26 | val newEnd = max(end, lastReplacementSpanEnd ?: end) 27 | return newStart to newEnd 28 | } 29 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/internal/utils/UriContentListener.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.internal.utils 2 | 3 | import android.content.ClipData 4 | import android.net.Uri 5 | import android.view.View 6 | import androidx.core.view.ContentInfoCompat 7 | import androidx.core.view.OnReceiveContentListener 8 | 9 | internal class UriContentListener( 10 | private val onContent: (uri: Uri) -> Unit, 11 | ) : OnReceiveContentListener { 12 | override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { 13 | val split = payload.partition { item -> item.uri != null } 14 | val uriContent = split.first 15 | val remaining = split.second 16 | 17 | if (uriContent != null) { 18 | val clip: ClipData = uriContent.clip 19 | for (i in 0 until clip.itemCount) { 20 | val uri = clip.getItemAt(i).uri 21 | // ... app-specific logic to handle the URI ... 22 | onContent(uri) 23 | } 24 | } 25 | // Return anything that we didn't handle ourselves. This preserves the default platform 26 | // behavior for text and anything else for which we are not implementing custom handling. 27 | return remaining 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/internal/view/models/LinkActionExt.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.internal.view.models 2 | 3 | 4 | import io.element.android.wysiwyg.view.models.LinkAction 5 | import uniffi.wysiwyg_composer.LinkAction as InternalLinkAction 6 | 7 | internal fun InternalLinkAction.toApiModel(): LinkAction? = 8 | when (this) { 9 | is InternalLinkAction.Edit -> LinkAction.SetLink(currentUrl = url) 10 | is InternalLinkAction.Create -> LinkAction.SetLink(currentUrl = null) 11 | is InternalLinkAction.CreateWithText -> LinkAction.InsertLink 12 | is InternalLinkAction.Disabled -> null 13 | } 14 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/internal/viewmodel/ReplaceTextResult.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.internal.viewmodel 2 | 3 | import android.text.Spanned 4 | import android.widget.EditText 5 | import uniffi.wysiwyg_composer.TextUpdate.ReplaceAll 6 | import uniffi.wysiwyg_composer.TextUpdate.Select 7 | 8 | /** 9 | * Result of a composer operation to be applied to the [EditText]. 10 | */ 11 | internal sealed interface ComposerResult { 12 | /** 13 | * Mapped model of [ReplaceAll] from the Rust code to be applied to the [EditText]. 14 | */ 15 | data class ReplaceText( 16 | /** Text in [Spanned] format after being parsed from HTML. */ 17 | val text: CharSequence, 18 | /** Selection to apply to the editor. */ 19 | val selection: IntRange, 20 | ) : ComposerResult 21 | 22 | /** 23 | * Mapped model of [Select] from the Rust code to be applied to the [EditText]. 24 | */ 25 | data class SelectionUpdated( 26 | /** Selection to apply to the editor. */ 27 | val selection: IntRange, 28 | ) : ComposerResult 29 | } 30 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/CharContants.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | /** 4 | * Constants for characters used as placeholders inside HTML. 5 | */ 6 | const val ZWSP: Char = '\u200B' 7 | const val NBSP: Char = '\u00A0' 8 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/HtmlConverter.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import io.element.android.wysiwyg.display.MentionDisplayHandler 6 | import io.element.android.wysiwyg.internal.utils.AndroidHtmlConverter 7 | import io.element.android.wysiwyg.view.StyleConfig 8 | 9 | interface HtmlConverter { 10 | 11 | fun fromHtmlToSpans(html: String): CharSequence 12 | 13 | object Factory { 14 | fun create( 15 | context: Context, 16 | styleConfig: StyleConfig, 17 | mentionDisplayHandler: MentionDisplayHandler?, 18 | isMention: ((text: String, url: String) -> Boolean)? = null, 19 | ): HtmlConverter { 20 | val resourcesProvider = AndroidResourcesHelper(context) 21 | return AndroidHtmlConverter(provideHtmlToSpansParser = { html -> 22 | HtmlToSpansParser( 23 | resourcesHelper = resourcesProvider, 24 | html = html, 25 | styleConfig = styleConfig, 26 | mentionDisplayHandler = mentionDisplayHandler, 27 | isMention = isMention, 28 | ) 29 | }) 30 | } 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/LoggingConfig.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | import io.element.android.wysiwyg.BuildConfig 4 | 5 | object LoggingConfig { 6 | var enableDebugLogs = BuildConfig.DEBUG 7 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/ResourcesHelper.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | import android.content.Context 4 | import android.util.DisplayMetrics 5 | import androidx.annotation.ColorRes 6 | import androidx.annotation.Dimension 7 | import androidx.core.content.res.ResourcesCompat 8 | 9 | /** 10 | * This class provides access to resources needed to convert HTML to spans. 11 | */ 12 | internal interface ResourcesHelper { 13 | fun getDisplayMetrics(): DisplayMetrics 14 | 15 | fun dpToPx(@Dimension(unit = Dimension.DP) dp: Int): Float 16 | 17 | fun getColor(@ColorRes colorId: Int): Int 18 | } 19 | 20 | /** 21 | * This class provides access to Android resources needed to convert HTML to spans. 22 | */ 23 | internal class AndroidResourcesHelper( 24 | private val context: Context, 25 | ) : ResourcesHelper { 26 | 27 | override fun getDisplayMetrics(): DisplayMetrics { 28 | return context.resources.displayMetrics 29 | } 30 | 31 | override fun dpToPx(dp: Int): Float { 32 | return dp * getDisplayMetrics().density 33 | } 34 | 35 | override fun getColor(colorId: Int): Int { 36 | return ResourcesCompat.getColor(context.resources, colorId, context.theme) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/RustCleanerTask.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | import io.element.android.wysiwyg.BuildConfig 4 | import timber.log.Timber 5 | import uniffi.wysiwyg_composer.Disposable 6 | 7 | internal class RustCleanerTask( 8 | private val disposable: Disposable, 9 | ) : Runnable { 10 | override fun run() { 11 | if (BuildConfig.DEBUG) { 12 | Timber.d("Cleaning up disposable: $disposable") 13 | } 14 | disposable.destroy() 15 | } 16 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/RustErrorCollector.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | /** 4 | * Callback for catching and dealing with Rust-related issues. 5 | */ 6 | fun interface RustErrorCollector { 7 | fun onRustError(throwable: Throwable) 8 | } 9 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/utils/ThrowableExt.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | import io.element.android.wysiwyg.BuildConfig 4 | 5 | fun Throwable.throwIfDebugBuild(): Unit = 6 | if (BuildConfig.DEBUG) { 7 | throw this 8 | } else { 9 | printStackTrace() 10 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/models/InlineFormat.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.models 2 | 3 | import androidx.compose.runtime.Immutable 4 | import uniffi.wysiwyg_composer.ComposerAction 5 | 6 | /** 7 | * Mapping of [ComposerAction] inline format actions. These are text styles that can be applied to 8 | * a text selection in the editor. 9 | */ 10 | @Immutable 11 | sealed interface InlineFormat { 12 | data object Bold: InlineFormat 13 | data object Italic: InlineFormat 14 | data object Underline: InlineFormat 15 | data object StrikeThrough: InlineFormat 16 | data object InlineCode: InlineFormat 17 | } 18 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/models/LinkAction.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.models 2 | 3 | /** 4 | * Link related editor actions, depending on the current selection. 5 | */ 6 | sealed class LinkAction { 7 | /** 8 | * Insert new text with a link (only available when no text is selected) 9 | */ 10 | data object InsertLink : LinkAction() 11 | 12 | /** 13 | * Add or change the link url for the current selection, without supplying text. 14 | */ 15 | data class SetLink(val currentUrl: String?) : LinkAction() 16 | } 17 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/BlockSpan.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | interface BlockSpan 4 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/CodeSpanConstants.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | internal object CodeSpanConstants { 4 | const val DEFAULT_RELATIVE_SIZE_PROPORTION = 0.87f 5 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/CustomMentionSpan.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.text.style.ReplacementSpan 6 | 7 | /** 8 | * Wrapper for a [ReplacementSpan] which does nothing except delegate to an 9 | * underlying span. 10 | * It is used to allow reuse of the same underlying span across multiple ranges 11 | * of a spanned text. 12 | */ 13 | class CustomMentionSpan( 14 | val providedSpan: ReplacementSpan, 15 | val url: String? = null, 16 | ) : ReplacementSpan() { 17 | override fun draw( 18 | canvas: Canvas, 19 | text: CharSequence?, 20 | start: Int, 21 | end: Int, 22 | x: Float, 23 | top: Int, 24 | y: Int, 25 | bottom: Int, 26 | paint: Paint 27 | ) = providedSpan.draw( 28 | canvas, text, start, end, x, top, y, bottom, paint 29 | ) 30 | 31 | override fun getSize( 32 | paint: Paint, 33 | text: CharSequence?, 34 | start: Int, 35 | end: Int, 36 | fm: Paint.FontMetricsInt? 37 | ): Int = providedSpan.getSize( 38 | paint, text, start, end, fm 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/ExtraCharacterSpan.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | import android.text.NoCopySpan 4 | import uniffi.wysiwyg_composer.ComposerModel 5 | 6 | /** 7 | * Special 'span' used as a marker to know there's a difference between the indexes in the Rust code 8 | * and the ones in the UI components. 9 | * 10 | * This is done because in the Dom, just using a `
  • ` tag will create a new line break in a list, 11 | * but for Android we have to add an extra `\n` line break character for list items to be rendered 12 | * properly. To fix this mismatch, we use this [ExtraCharacterSpan] to understand when a line break 13 | * character should be left as is, because it exists in the [ComposerModel] or if we should handle 14 | * it in a special way because it's not present in the HTML output. 15 | */ 16 | class ExtraCharacterSpan: NoCopySpan 17 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/LinkSpan.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | import android.text.TextPaint 4 | import android.text.style.URLSpan 5 | 6 | internal class LinkSpan( 7 | url: String 8 | ) : URLSpan(url), PlainAtRoomMentionDisplaySpan { 9 | override fun updateDrawState(ds: TextPaint) { 10 | // Check if the text is already underlined (for example by an UnderlineSpan) 11 | val wasUnderlinedByAnotherSpan = ds.isUnderlineText 12 | 13 | super.updateDrawState(ds) 14 | 15 | if (!wasUnderlinedByAnotherSpan) { 16 | ds.isUnderlineText = false 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/PlainAtRoomMentionDisplaySpan.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | /** 4 | * Used to override any [MentionDisplayHandler] and force text to be plain. 5 | * This can be used, for example, inside a code block where text must be displayed as-is. 6 | */ 7 | internal interface PlainAtRoomMentionDisplaySpan -------------------------------------------------------------------------------- /platforms/android/library/src/main/java/io/element/android/wysiwyg/view/spans/ReuseSourceSpannableFactory.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.view.spans 2 | 3 | import android.text.Spannable 4 | import android.text.SpannableStringBuilder 5 | 6 | /** 7 | * This factory is used to reuse the current source if possible to improve performance. 8 | */ 9 | internal class ReuseSourceSpannableFactory : Spannable.Factory() { 10 | override fun newSpannable(source: CharSequence?): Spannable { 11 | // Try to reuse current source if possible to improve performance 12 | return source as? Spannable ?: SpannableStringBuilder(source) 13 | } 14 | } -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/drawable/code_block_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/drawable/inline_code_multi_line_bg_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/drawable/inline_code_multi_line_bg_mid.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/drawable/inline_code_multi_line_bg_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/drawable/inline_code_single_line_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #21262C 4 | #394049 5 | #21262C 6 | 7 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #F4F6FA 4 | #E3E8F0 5 | #E3E8F0 6 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 1dp 5 | 2dp 6 | 1dp 7 | 8 | -------------------------------------------------------------------------------- /platforms/android/library/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.87 4 | -------------------------------------------------------------------------------- /platforms/android/library/src/test/kotlin/io/element/android/wysiwyg/mocks/MockComposerUpdateFactory.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.mocks 2 | 3 | import io.mockk.every 4 | import io.mockk.mockk 5 | import uniffi.wysiwyg_composer.ComposerUpdate 6 | import uniffi.wysiwyg_composer.LinkActionUpdate 7 | import uniffi.wysiwyg_composer.MenuAction 8 | import uniffi.wysiwyg_composer.MenuState 9 | import uniffi.wysiwyg_composer.TextUpdate 10 | 11 | object MockComposerUpdateFactory { 12 | fun create( 13 | menuAction: MenuAction = MenuAction.Keep, 14 | menuState: MenuState = MenuState.Keep, 15 | textUpdate: TextUpdate = TextUpdate.Keep, 16 | linkAction: LinkActionUpdate = LinkActionUpdate.Keep, 17 | ): ComposerUpdate = mockk { 18 | every { menuAction() } returns menuAction 19 | every { menuState() } returns menuState 20 | every { textUpdate() } returns textUpdate 21 | every { linkAction() } returns linkAction 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /platforms/android/library/src/test/kotlin/io/element/android/wysiwyg/mocks/MockTextUpdateFactory.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.mocks 2 | 3 | import io.element.android.wysiwyg.extensions.toUShortList 4 | import uniffi.wysiwyg_composer.TextUpdate 5 | 6 | object MockTextUpdateFactory { 7 | fun createReplaceAll( 8 | html: String = "", 9 | start: Int = 0, 10 | end: Int = 0, 11 | ) = TextUpdate.ReplaceAll( 12 | replacementHtml = html.toUShortList(), 13 | startUtf16Codeunit = start.toUInt(), 14 | endUtf16Codeunit = end.toUInt() 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /platforms/android/library/src/test/kotlin/io/element/android/wysiwyg/test/utils/SpanUtils.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.utils 2 | 3 | import android.text.TextUtils 4 | 5 | fun CharSequence.dumpSpans(): List { 6 | val spans = mutableListOf() 7 | TextUtils.dumpSpans( 8 | this, { span -> 9 | val spanWithoutHash = span.split(" ").filterIndexed { index, _ -> 10 | index != 1 11 | }.joinToString(" ") 12 | 13 | spans.add(spanWithoutHash) 14 | }, "" 15 | ) 16 | return spans 17 | } -------------------------------------------------------------------------------- /platforms/android/library/src/test/kotlin/io/element/android/wysiwyg/utils/AndroidHtmlConverterTest.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | import androidx.core.text.toSpanned 4 | import io.element.android.wysiwyg.internal.utils.AndroidHtmlConverter 5 | import io.mockk.every 6 | import io.mockk.mockk 7 | import org.hamcrest.MatcherAssert.assertThat 8 | import org.hamcrest.Matchers.equalTo 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.robolectric.RobolectricTestRunner 12 | 13 | @RunWith(RobolectricTestRunner::class) 14 | class AndroidHtmlConverterTest { 15 | private val htmlToSpansParser = mockk() 16 | private val androidHtmlConverter = AndroidHtmlConverter( 17 | provideHtmlToSpansParser = { htmlToSpansParser } 18 | ) 19 | 20 | @Test 21 | fun testToSpans() { 22 | val expectedParserOutput = "mock parser output".toSpanned() 23 | every { htmlToSpansParser.convert() } returns expectedParserOutput 24 | 25 | val result = androidHtmlConverter.fromHtmlToSpans("input") 26 | 27 | assertThat(result, equalTo(expectedParserOutput)) 28 | } 29 | } -------------------------------------------------------------------------------- /platforms/android/library/src/test/kotlin/io/element/android/wysiwyg/utils/BasicHtmlConverter.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.utils 2 | 3 | /** 4 | * HTML converter that is not depend on Android, for unit tests. 5 | */ 6 | class BasicHtmlConverter: HtmlConverter { 7 | 8 | override fun fromHtmlToSpans(html: String): CharSequence = html.replace("<[^>]*>".toRegex(), "") 9 | } -------------------------------------------------------------------------------- /platforms/android/plugins/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | repositories { 3 | mavenCentral() 4 | } 5 | versionCatalogs { 6 | create("libs") { 7 | from(files("../gradle/libs.versions.toml")) 8 | } 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /platforms/android/scripts/ci_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit immediately if unit tests fail 4 | set -e 5 | 6 | ./gradlew unitTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES 7 | ./gradlew generateUnitTestCoverageReport $CI_GRADLE_ARG_PROPERTIES 8 | 9 | # Don't exit immediately from UI test failure to collect screenshots 10 | set +e 11 | 12 | ./gradlew instrumentationTestsWithCoverage $CI_GRADLE_ARG_PROPERTIES 13 | 14 | UI_TEST_EXIT_CODE=$? 15 | if [ $UI_TEST_EXIT_CODE -ne 0 ]; then 16 | echo "UI tests failed." 17 | echo "Pulling screenshots from device..." 18 | adb shell ls /sdcard/Pictures/UiTest/ 19 | mkdir build/reports/screenshots 20 | adb pull /sdcard/Pictures/UiTest/ build/reports/screenshots/ 21 | exit $UI_TEST_EXIT_CODE 22 | fi 23 | set -e 24 | 25 | ./gradlew generateInstrumentationTestCoverageReport $CI_GRADLE_ARG_PROPERTIES 26 | 27 | -------------------------------------------------------------------------------- /platforms/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Rich Text Editor" 16 | include ':example-view', ':example-compose', ':library', ':library-compose', ':test' 17 | -------------------------------------------------------------------------------- /platforms/android/test/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /platforms/android/test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.android.library) 4 | alias(libs.plugins.kotlin.android) 5 | } 6 | 7 | android { 8 | namespace = "io.element.android.wysiwyg.test" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 21 13 | } 14 | 15 | buildTypes { 16 | release { 17 | isMinifyEnabled = false 18 | } 19 | } 20 | compileOptions { 21 | sourceCompatibility = JavaVersion.VERSION_11 22 | targetCompatibility = JavaVersion.VERSION_11 23 | } 24 | } 25 | 26 | kotlin { 27 | jvmToolchain(11) 28 | } 29 | 30 | dependencies { 31 | implementation(libs.test.androidx.uiautomator) 32 | implementation(libs.test.junit) 33 | implementation(libs.test.androidx.espresso) 34 | } 35 | -------------------------------------------------------------------------------- /platforms/android/test/src/main/java/io/element/android/wysiwyg/test/rules/DismissAnrRule.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.rules 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation 4 | import androidx.test.uiautomator.UiDevice 5 | import androidx.test.uiautomator.UiObject 6 | import androidx.test.uiautomator.UiSelector 7 | import org.junit.rules.TestWatcher 8 | import org.junit.runner.Description 9 | 10 | internal class DismissAnrRule : TestWatcher() { 11 | override fun starting(description: Description) { 12 | dismissAnr() 13 | } 14 | } 15 | 16 | private fun dismissAnr() { 17 | val device = UiDevice.getInstance(getInstrumentation()) 18 | val dialog = device.findAnrDialog() 19 | if (dialog.exists()) { 20 | device.findWaitButton().click() 21 | } 22 | } 23 | 24 | private fun UiDevice.findAnrDialog(): UiObject = 25 | findObject(UiSelector().textContains("isn't responding")) 26 | 27 | private fun UiDevice.findWaitButton(): UiObject = 28 | findObject(UiSelector().text("Wait").enabled(true)) 29 | .apply { waitForExists(5000) } -------------------------------------------------------------------------------- /platforms/android/test/src/main/java/io/element/android/wysiwyg/test/rules/FlakyEmulatorRule.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.rules 2 | 3 | import org.junit.rules.RuleChain 4 | import org.junit.rules.TestRule 5 | 6 | /** 7 | * Creates a rule that helps to reduce emulator related flakiness. 8 | */ 9 | fun createFlakyEmulatorRule(retry: Boolean = true): TestRule = if (retry) { 10 | RuleChain 11 | .outerRule(RetryOnFailureRule()) 12 | .around(DismissAnrRule()) 13 | } else { 14 | RuleChain 15 | .outerRule(DismissAnrRule()) 16 | } 17 | -------------------------------------------------------------------------------- /platforms/android/test/src/main/java/io/element/android/wysiwyg/test/rules/RetryOnFailureRule.kt: -------------------------------------------------------------------------------- 1 | package io.element.android.wysiwyg.test.rules 2 | 3 | import org.junit.rules.TestRule 4 | import org.junit.runner.Description 5 | import org.junit.runners.model.Statement 6 | 7 | internal class RetryOnFailureRule : TestRule { 8 | override fun apply( 9 | base: Statement, 10 | description: Description 11 | ): Statement = 12 | RetryStatement(base) 13 | } 14 | 15 | private class RetryStatement(private val base: Statement) : Statement() { 16 | override fun evaluate() { 17 | try { 18 | base.evaluate() 19 | return 20 | } catch (t: Throwable) { 21 | base.evaluate() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /platforms/ios/example/.gitignore: -------------------------------------------------------------------------------- 1 | # ENV 2 | build/ 3 | DerivedData/ 4 | xcuserdata 5 | *.xcuserstate 6 | .vscode/ 7 | vendor/ 8 | .DS_Store -------------------------------------------------------------------------------- /platforms/ios/example/.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.6 2 | 3 | --disable wrapMultiLineStatementBraces 4 | --disable hoistPatternLet 5 | 6 | --ifdef no-indent 7 | --nospaceoperators ...,..< 8 | --stripunusedargs closure-only 9 | --trimwhitespace nonblank-lines 10 | --wrapparameters after-first 11 | --redundanttype inferred 12 | --emptybraces spaced -------------------------------------------------------------------------------- /platforms/ios/example/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # rule identifiers to exclude from running 2 | disabled_rules: 3 | - todo 4 | - trailing_whitespace 5 | - unused_setter_value 6 | - redundant_discardable_let 7 | - identifier_name 8 | - trailing_comma 9 | 10 | # some rules are only opt-in 11 | opt_in_rules: 12 | - force_unwrapping 13 | - private_action 14 | - explicit_init 15 | 16 | line_length: 17 | warning: 140 18 | error: 200 -------------------------------------------------------------------------------- /platforms/ios/example/Brewfile: -------------------------------------------------------------------------------- 1 | brew "swiftformat" 2 | brew "swiftlint" 3 | -------------------------------------------------------------------------------- /platforms/ios/example/Shared/WysiwygSharedConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | enum WysiwygSharedConstants { 20 | static let composerMinHeight: CGFloat = 22.0 21 | static let composerMaxExtendedHeight: CGFloat = 250.0 22 | } 23 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg.xcodeproj/project.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Copyright ___YEAR___ The Matrix.org Foundation C.I.C 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | 22 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg/Extensions/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | public extension View { 20 | func alert(isPresented: Binding, _ alert: AlertConfig) -> some View { 21 | AlertHelper(isPresented: isPresented, alert: alert, content: self) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSSupportsOpeningDocumentsInPlace 6 | 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeName 11 | Mention Pills 12 | CFBundleTypeRole 13 | Viewer 14 | LSHandlerRank 15 | Owner 16 | LSItemContentTypes 17 | 18 | org.matrix.rte.pills 19 | 20 | 21 | 22 | UTExportedTypeDeclarations 23 | 24 | 25 | UTTypeConformsTo 26 | 27 | public.text 28 | 29 | UTTypeDescription 30 | Mention Pills 31 | UTTypeIdentifier 32 | org.matrix.rte.pills 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /platforms/ios/example/Wysiwyg/WysiwygApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SwiftUI 18 | 19 | @main 20 | struct WysiwygApp: App { 21 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 22 | 23 | var body: some Scene { 24 | WindowGroup { 25 | ContentView() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /platforms/ios/example/WysiwygUITests/WysiwygUITests+CodeBlocks.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | extension WysiwygUITests { 20 | func testCodeBlock() throws { 21 | // Type something into composer. 22 | textView.typeTextCharByChar("Some text") 23 | button(.codeBlockButton).tap() 24 | assertTextViewContent("Some text") 25 | 26 | assertTreeEquals( 27 | """ 28 | └>codeblock 29 | └>p 30 | └>"Some text" 31 | """ 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /platforms/ios/example/WysiwygUITests/WysiwygUITests+Quotes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import XCTest 18 | 19 | extension WysiwygUITests { 20 | func testQuote() throws { 21 | // Type something into composer. 22 | textView.typeTextCharByChar("Some text") 23 | button(.quoteButton).tap() 24 | assertTextViewContent("Some text") 25 | 26 | assertTreeEquals( 27 | """ 28 | └>blockquote 29 | └>p 30 | └>"Some text" 31 | """ 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /platforms/ios/example/ios-test-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | xcodebuild \ 6 | -project Wysiwyg.xcodeproj \ 7 | -scheme WysiwygComposerTests \ 8 | -sdk iphonesimulator \ 9 | -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' \ 10 | -derivedDataPath ./DerivedData \ 11 | -resultBundlePath tests.xcresult \ 12 | -enableCodeCoverage YES \ 13 | test 14 | -------------------------------------------------------------------------------- /platforms/ios/example/ios-ui-test-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | xcodebuild \ 6 | -project Wysiwyg.xcodeproj \ 7 | -scheme Wysiwyg \ 8 | -sdk iphonesimulator \ 9 | -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' \ 10 | -derivedDataPath ./DerivedData \ 11 | -resultBundlePath ui-tests.xcresult \ 12 | -enableCodeCoverage YES \ 13 | test 14 | 15 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/.gitignore: -------------------------------------------------------------------------------- 1 | # ENV 2 | .DS_Store 3 | /.build 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | DerivedData/ 8 | .swiftpm/config/registries.json 9 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 10 | .netrc 11 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/.swiftformat: -------------------------------------------------------------------------------- 1 | --swiftversion 5.6 2 | 3 | --disable wrapMultiLineStatementBraces 4 | --disable hoistPatternLet 5 | 6 | --ifdef no-indent 7 | --nospaceoperators ...,..< 8 | --stripunusedargs closure-only 9 | --trimwhitespace nonblank-lines 10 | --wrapparameters after-first 11 | --redundanttype inferred 12 | --emptybraces spaced -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # rule identifiers to exclude from running 2 | disabled_rules: 3 | - todo 4 | - trailing_whitespace 5 | - unused_setter_value 6 | - redundant_discardable_let 7 | - identifier_name 8 | - trailing_comma 9 | 10 | # some rules are only opt-in 11 | opt_in_rules: 12 | - force_unwrapping 13 | - private_action 14 | - explicit_init 15 | 16 | # paths to ignore during linting. Takes precedence over `included`. 17 | excluded: 18 | - Sources/WysiwygComposer/WysiwygComposer.swift 19 | 20 | line_length: 21 | warning: 140 22 | error: 200 -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Copyright ___YEAR___ The Matrix.org Foundation C.I.C 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | 22 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | **If you've found a security vulnerability, please report it to security@matrix.org** 4 | 5 | For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/ 6 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/DTCoreTextExtended/include/UIFont+AttributedStringBuilder.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 New Vector Ltd 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | @import UIKit; 18 | 19 | NS_ASSUME_NONNULL_BEGIN 20 | 21 | @interface UIFont(DTCoreTextFix) 22 | 23 | // Fix DTCoreText iOS 13 issue (https://github.com/Cocoanetics/DTCoreText/issues/1168) 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/BuildHTMLAttributedError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | /// Describe an error occurring during HTML string build. 20 | public enum BuildHtmlAttributedError: LocalizedError, Equatable { 21 | /// Encoding data from raw HTML input failed. 22 | case dataError(encoding: String.Encoding) 23 | 24 | public var errorDescription: String? { 25 | switch self { 26 | case let .dataError(encoding: encoding): 27 | return "Unable to encode string with: \(encoding.description) rawValue: \(encoding.rawValue)" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/CGRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import CoreGraphics 18 | 19 | extension CGRect { 20 | /// Return a copy of the rect extended to the leading and trailing borders of given frame. 21 | /// 22 | /// - Parameters: 23 | /// - frame: frame to extend into. 24 | /// - verticalPadding: padding to apply vertically 25 | func extendHorizontally(in frame: CGRect, withVerticalPadding verticalPadding: CGFloat) -> CGRect { 26 | CGRect(x: frame.minX, y: minY - verticalPadding, width: frame.width, height: height + 2 * verticalPadding) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSParagraphStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | extension NSParagraphStyle { 20 | /// Returns a mutable copy of self, casted as `NSMutableParagraphStyle`. 21 | func mut() -> NSMutableParagraphStyle { 22 | guard let mut = mutableCopy() as? NSMutableParagraphStyle else { 23 | return NSMutableParagraphStyle() 24 | } 25 | return mut 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/Extensions/NSRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | extension NSRange { 20 | /// Returns an `NSRange` with the length reduced by 1. 21 | var excludingLast: NSRange { 22 | .init(location: location, length: length - 1) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/HTMLParserHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | /// Temporary color used to detect the range of the HTML element inside the attributed string. 20 | enum TempColor { 21 | static let inlineCode: UIColor = .red 22 | static let codeBlock: UIColor = .green 23 | static let quote: UIColor = .blue 24 | } 25 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/HTMLParser/MentionContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | /// A struct that can be used as an attribute to persist the original content of a replaced part of an `NSAttributedString`. 18 | struct MentionContent { 19 | /// The length of the replaced content in the Rust model. 20 | let rustLength: Int 21 | 22 | let url: String 23 | } 24 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Components/WysiwygKeyCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | /// An class that describes key commands that can be handled by the hosting application wth their associated action 20 | public struct WysiwygKeyCommand { 21 | /// A default initialiser for the enter command which is most commonly used 22 | public static func enter(action: @escaping () -> Void) -> WysiwygKeyCommand { 23 | WysiwygKeyCommand(input: "\r", modifierFlags: [], action: action) 24 | } 25 | 26 | let input: String 27 | let modifierFlags: UIKeyModifierFlags 28 | let action: () -> Void 29 | } 30 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Components/WysiwygLinkOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | public enum WysiwygLinkOperation: Equatable { 20 | case setLink(urlString: String) 21 | case createLink(urlString: String, text: String) 22 | case removeLinks 23 | } 24 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Extensions/ComposerAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | extension ComposerAction { 18 | /// Returns `true` if action requires all current formatting to be re-applied on 19 | /// next character stroke when triggered on an empty selection. 20 | var requiresReapplyFormattingOnEmptySelection: Bool { 21 | switch self { 22 | case .bold, .italic, .strikeThrough, .underline, .inlineCode, .link, .undo, .redo: 23 | return false 24 | case .orderedList, .unorderedList, .indent, .unindent, .codeBlock, .quote: 25 | return true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Extensions/NSRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | public extension NSRange { 20 | /// Returns a range at starting location, i.e. {0, 0}. 21 | static let zero = Self(location: 0, length: 0) 22 | } 23 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Extensions/PatternKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | public extension PatternKey { 18 | /// Associated mention type, if any. 19 | var mentionType: WysiwygMentionType? { 20 | switch self { 21 | case .at: 22 | return .user 23 | case .hash: 24 | return .room 25 | case .slash, .custom: 26 | return nil 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Extensions/String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import Foundation 18 | 19 | extension String { 20 | /// Returns length of the string in UTF16 code units. 21 | var utf16Length: Int { 22 | (self as NSString).length 23 | } 24 | 25 | /// Converts all whitespaces to NBSP to avoid diffs caused by HTML translations. 26 | var withNBSP: String { 27 | String(map { $0.isWhitespace ? Character.nbsp : $0 }) 28 | } 29 | 30 | var containsLatinAndCommonCharactersOnly: Bool { 31 | range(of: "[^\\p{Latin}\\p{Common}]", options: .regularExpression) == nil 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Sources/WysiwygComposer/Extensions/UITextView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import UIKit 18 | 19 | extension UITextView { 20 | /// Toggles autocorrection if needed. It should always be enabled, 21 | /// unless current text starts with exactly one slash. 22 | func toggleAutocorrectionIfNeeded() { 23 | let newValue: UITextAutocorrectionType = attributedText.string.prefix(while: { $0 == .slash }).count == 1 ? .no : .yes 24 | if newValue != autocorrectionType { 25 | autocorrectionType = newValue 26 | reloadInputViews() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/SnapshotTests+Links.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2023 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | import SnapshotTesting 18 | 19 | final class LinksSnapshotTests: SnapshotTests { 20 | func testLinkContent() throws { 21 | viewModel.setHtmlContent("test") 22 | assertSnapshot( 23 | matching: hostingController, 24 | as: .image(on: .iPhone13), 25 | record: isRecord 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testCodeBlockContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testCodeBlockContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testInlineCodeContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testInlineCodeContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testMultipleBlocksContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testMultipleBlocksContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testQuoteContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Blocks/testQuoteContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Common/testClearState.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Common/testClearState.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Common/testPlainTextContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Common/testPlainTextContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Links/testLinkContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Links/testLinkContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testIndentedListContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testIndentedListContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testListInQuote.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testListInQuote.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testMultipleListsContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testMultipleListsContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testOrderedListContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testOrderedListContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testUnorderedListContent.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matrix-org/matrix-rich-text-editor/cb62b25558d3906faf4c8942813e9000154d358e/platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerSnapshotTests/__Snapshots__/SnapshotTests+Lists/testUnorderedListContent.1.png -------------------------------------------------------------------------------- /platforms/ios/lib/WysiwygComposer/Tests/WysiwygComposerTests/TestConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2022 The Matrix.org Foundation C.I.C 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | enum TestConstants { 18 | /// Test string with emojis inputed both with codepoints and Xcode emoji insertion. 19 | /// String is actually 6 char long "abc🎉🎉👩🏿‍🚀" and represents 14 UTF-16 code units (3+2+2+7) 20 | static let testStringWithEmojis = "abc🎉\u{1f389}\u{1F469}\u{1F3FF}\u{200D}\u{1F680}" 21 | static let testStringAfterBackspace = "abc🎉🎉" 22 | } 23 | -------------------------------------------------------------------------------- /platforms/ios/tools/release/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 9 | .netrc 10 | -------------------------------------------------------------------------------- /platforms/ios/tools/release/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-argument-parser", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-argument-parser.git", 7 | "state" : { 8 | "revision" : "46989693916f56d1186bd59ac15124caef896560", 9 | "version" : "1.3.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-command-line-tools", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/element-hq/swift-command-line-tools.git", 16 | "state" : { 17 | "revision" : "483396af716a59eb45379126389a063f7f9bee80" 18 | } 19 | } 20 | ], 21 | "version" : 2 22 | } 23 | -------------------------------------------------------------------------------- /platforms/ios/tools/release/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Release", 6 | platforms: [ 7 | .macOS(.v13) 8 | ], 9 | products: [ 10 | .executable(name: "release", targets: ["Release"]) 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), 14 | .package(url: "https://github.com/element-hq/swift-command-line-tools.git", revision: "483396af716a59eb45379126389a063f7f9bee80") 15 | // .package(path: "../../../../../swift-command-line-tools") 16 | ], 17 | targets: [ 18 | .executableTarget(name: "Release", 19 | dependencies: [ 20 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 21 | .product(name: "CommandLineTools", package: "swift-command-line-tools") 22 | ]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /platforms/web/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | insert_final_newline = true 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 4 7 | max_line_length = 80 8 | quote_type = single 9 | -------------------------------------------------------------------------------- /platforms/web/.eslintignore: -------------------------------------------------------------------------------- 1 | src/images 2 | generated 3 | dist 4 | dist-demo 5 | cypress.config.ts 6 | vite-env.d.ts 7 | vite.config.ts 8 | vite.demo.config.ts 9 | scripts 10 | cypress 11 | example-wysiwyg 12 | coverage 13 | .eslintignore 14 | -------------------------------------------------------------------------------- /platforms/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["matrix-org", "prettier"], 3 | "extends": [ 4 | "plugin:matrix-org/typescript", 5 | "plugin:matrix-org/react", 6 | "plugin:matrix-org/a11y", 7 | "prettier" 8 | ], 9 | "parserOptions": { 10 | "project": ["./tsconfig.json"] 11 | }, 12 | "env": { 13 | "browser": true, 14 | "node": true 15 | }, 16 | "settings": { 17 | "react": { 18 | "version": "detect" 19 | } 20 | }, 21 | "rules": { 22 | "react/jsx-curly-spacing": "off", 23 | "new-cap": "off", 24 | "@typescript-eslint/naming-convention": [ 25 | "error", 26 | { 27 | "selector": ["variable", "function"], 28 | "modifiers": ["private"], 29 | "format": ["camelCase"], 30 | "leadingUnderscore": "allow" 31 | } 32 | ], 33 | "max-len": ["error", { "code": 120, "ignoreUrls": true }], 34 | "matrix-org/require-copyright-header": "error", 35 | "prettier/prettier": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /platforms/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-demo 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | /generated/ 28 | /coverage/ 29 | /cypress/screenshots/ 30 | /cypress/videos/ 31 | -------------------------------------------------------------------------------- /platforms/web/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-demo 3 | example-wysiwyg 4 | generated 5 | -------------------------------------------------------------------------------- /platforms/web/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "quoteProps": "consistent" 4 | } 5 | -------------------------------------------------------------------------------- /platforms/web/cypress.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { defineConfig } from 'cypress'; 18 | 19 | export default defineConfig({ 20 | videoUploadOnPasses: false, 21 | //projectId: 'ppvnzg', 22 | experimentalInteractiveRunEvents: true, 23 | defaultCommandTimeout: 10000, 24 | chromeWebSecurity: false, 25 | e2e: { 26 | baseUrl: 'http://localhost:5173', 27 | specPattern: 'cypress/e2e/**/*.{ts,tsx}', 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /platforms/web/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /// 18 | 19 | import './commands'; 20 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/README.md: -------------------------------------------------------------------------------- 1 | # example-wysiwyg 2 | 3 | A minimal example of how to use matrix-wysiwyg in a React project. 4 | 5 | ## Set up and run 6 | 7 | ```bash 8 | npm install 9 | npm run dev 10 | ``` 11 | 12 | and open the URL that is printed on the console. 13 | 14 | ## Creating this project from scratch 15 | 16 | * Create the project: 17 | ```bash 18 | npm create vite@latest example-wysiwyg -- --template react-ts 19 | cd example-wysiwyg 20 | npm install 21 | npm add '@matrix-org/matrix-wysiwyg' 22 | ``` 23 | * Edit example-wysiwyg/src/App.tsx to look how it looks in this repo. 24 | * Edit example-wysiwyg/index.html to set the page title and remove favicon. 25 | * Delete example-wysiwyg/public/vite.svg and 26 | example-wysiwyg/src/assets/react.svg. 27 | * Disable eslint for a couple of files. 28 | * Follow the instructions in "Set up and run". 29 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Minimal Rich Text Editor Example 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-wysiwyg", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@matrix-org/matrix-wysiwyg": "^0.23.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.17", 18 | "@types/react-dom": "^18.0.6", 19 | "@vitejs/plugin-react": "^2.1.0", 20 | "typescript": "^4.6.4", 21 | "vite": "^3.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/src/main.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom/client' 4 | import App from './App' 5 | import './index.css' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /platforms/web/example-wysiwyg/vite.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()] 8 | }) 9 | -------------------------------------------------------------------------------- /platforms/web/lib/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export const ACTION_TYPES = [ 18 | 'bold', 19 | 'italic', 20 | 'strikeThrough', 21 | 'underline', 22 | 'undo', 23 | 'redo', 24 | 'orderedList', 25 | 'unorderedList', 26 | 'inlineCode', 27 | 'clear', 28 | 'link', 29 | 'codeBlock', 30 | 'quote', 31 | 'indent', 32 | 'unindent', 33 | ] as const; 34 | 35 | export const SUGGESTIONS = ['@', '#', '/', ''] as const; 36 | -------------------------------------------------------------------------------- /platforms/web/lib/testUtils/selection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { selectContent } from '../dom'; 18 | 19 | export function select( 20 | editor: HTMLDivElement, 21 | startIndex: number, 22 | endIndex: number, 23 | ): void { 24 | selectContent(editor, startIndex, endIndex); 25 | 26 | // the event is not automatically fired in jest 27 | document.dispatchEvent(new CustomEvent('selectionchange')); 28 | } 29 | 30 | export function deleteRange( 31 | editor: HTMLDivElement, 32 | start: number, 33 | end: number, 34 | ): void { 35 | select(editor, start, end); 36 | const sel = document.getSelection(); 37 | sel?.deleteFromDocument(); 38 | } 39 | -------------------------------------------------------------------------------- /platforms/web/lib/useListeners/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export { useListeners } from './useListeners'; 18 | export { sendWysiwygInputEvent } from './event'; 19 | -------------------------------------------------------------------------------- /platforms/web/lib/useTestCases/assert.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { SelectTuple, Tuple } from './types'; 18 | 19 | export function isSelectTuple(tuple: Tuple): tuple is SelectTuple { 20 | return tuple[0] === 'select'; 21 | } 22 | -------------------------------------------------------------------------------- /platforms/web/lib/useTestCases/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export { useTestCases } from './useTestCases'; 18 | -------------------------------------------------------------------------------- /platforms/web/lib/useTestCases/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { useTestCases } from './useTestCases'; 18 | 19 | export type TestUtilities = ReturnType['utilities']; 20 | export type SelectTuple = ['select', number, number]; 21 | export type Tuple = 22 | | SelectTuple 23 | | [string, (string | number)?, (string | number)?]; 24 | export type Actions = Array; 25 | -------------------------------------------------------------------------------- /platforms/web/lib/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /platforms/web/scripts/hack_exports.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | Modify dist/index.d.ts, so all the types are exported. 17 | */ 18 | 19 | import replace from 'replace'; 20 | 21 | console.log('[hack_exports] Hacking generated types to make all exported'); 22 | 23 | replace({ 24 | regex: /^declare/gm, 25 | replacement: 'export declare', 26 | paths: ['./dist/index.d.ts'], 27 | recursive: false, 28 | silent: false, 29 | }); 30 | -------------------------------------------------------------------------------- /platforms/web/src/images/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/web/src/images/indent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /platforms/web/src/images/inline_code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /platforms/web/src/images/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /platforms/web/src/images/strike_through.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /platforms/web/src/images/unindent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /platforms/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import ReactDOM from 'react-dom/client'; 19 | 20 | import App from './App'; 21 | 22 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 23 | 24 | 25 | , 26 | ); 27 | -------------------------------------------------------------------------------- /platforms/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /platforms/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "experimentalDecorators": false, 19 | "emitDecoratorMetadata": false, 20 | "noImplicitAny": true, 21 | "noUnusedLocals": true, 22 | "sourceMap": false, 23 | "outDir": "./dist", 24 | "declaration": true, 25 | "strictNullChecks": true, 26 | "strictFunctionTypes": true, 27 | "strictBindCallApply": true, 28 | "strictPropertyInitialization": true, 29 | "noImplicitThis": true, 30 | "alwaysStrict": true, 31 | "types": ["vitest/globals", "jest"] 32 | }, 33 | "include": ["src", "lib", "./test.setup.ts"], 34 | "references": [{ "path": "./tsconfig.node.json" }] 35 | } 36 | -------------------------------------------------------------------------------- /platforms/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"], 9 | "skipLibCheck": true 10 | } 11 | -------------------------------------------------------------------------------- /platforms/web/vite.demo.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Matrix.org Foundation C.I.C. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import react from '@vitejs/plugin-react'; 18 | import { defineConfig } from 'vite'; 19 | 20 | export default defineConfig({ 21 | plugins: [react()], 22 | base: '', 23 | build: { 24 | rollupOptions: { 25 | output: { 26 | dir: 'dist-demo', 27 | }, 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "ignoreDeps": ["Cocoanetics/DTCoreText"], 7 | "packageRules" : [ 8 | { 9 | "matchManagers": ["github-actions"], 10 | "groupName" : "GitHub Actions" 11 | }, 12 | { 13 | "matchManagers": ["swift", "cocoapods"], 14 | "groupName" : "Swift" 15 | }, 16 | { 17 | "matchManagers": ["gradle"], 18 | "groupName" : "Android" 19 | }, 20 | { 21 | "matchManagers": ["npm"], 22 | "groupName" : "Web" 23 | }, 24 | { 25 | "matchManagers": ["cargo"], 26 | "groupName" : "Rust" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.76" 3 | components = ["rustfmt"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=matrix-rich-text-editor 2 | sonar.organization=matrix-org 3 | 4 | # set the source code to be everything in /web/src/ and /web/lib/ folders 5 | sonar.sources=platforms/web/src,platforms/web/lib 6 | # then do not consider any /testUtils files or any .test. files as source code 7 | sonar.exclusions=platforms/web/lib/testUtils/**/*,platforms/web/lib/**/*.test.* 8 | 9 | # set the tests to be everything in /web/lib/ folder 10 | sonar.tests=platforms/web/lib 11 | # then only consider .test. files as test code 12 | sonar.test.inclusions=platforms/web/lib/**/*.test.* 13 | 14 | sonar.typescript.tsconfigPath=platforms/web/tsconfig.json 15 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 16 | sonar.coverage.exclusions=platforms/web/**/*.test.* 17 | sonar.testExecutionReportPaths=coverage/sonar-report.xml 18 | -------------------------------------------------------------------------------- /uniffi-bindgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uniffi-bindgen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | uniffi = { workspace = true, features = ["cli"] } 9 | -------------------------------------------------------------------------------- /uniffi-bindgen/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | uniffi::uniffi_bindgen_main() 3 | } 4 | -------------------------------------------------------------------------------- /update_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if (($# != 1)); then 4 | echo "There should be a single version argument passed." 5 | exit 1 6 | fi 7 | 8 | if [[ "$OSTYPE" == "darwin"* ]]; then 9 | if ! command -v gsed &> /dev/null; then 10 | echo "GNU-SED not found. Please install it using `brew install gnu-sed`." 11 | exit 1 12 | fi 13 | SED_CMD='gsed -i' 14 | else 15 | SED_CMD="sed -i" 16 | fi 17 | 18 | SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 19 | 20 | VERSION=$1 21 | CARGO_REGEX="s/^version\s*=\s*\".*\"/version = \"$VERSION\"/g" 22 | PACKAGE_JSON_REGEX="s/\"version\":\s*\".*\"/\"version\": \"$VERSION\"/g" 23 | GRADLE_PROPERTIES_REGEX="s/^VERSION_NAME=.*$/VERSION_NAME=$VERSION/g" 24 | 25 | echo "Updating Rust" 26 | $SED_CMD "$CARGO_REGEX" $SCRIPT_PATH/bindings/wysiwyg-ffi/Cargo.toml 27 | $SED_CMD "$CARGO_REGEX" $SCRIPT_PATH/bindings/wysiwyg-wasm/Cargo.toml 28 | $SED_CMD "$CARGO_REGEX" $SCRIPT_PATH/crates/wysiwyg/Cargo.toml 29 | 30 | echo "Updating Web" 31 | $SED_CMD "$PACKAGE_JSON_REGEX" $SCRIPT_PATH/platforms/web/package.json 32 | $SED_CMD "$PACKAGE_JSON_REGEX" $SCRIPT_PATH/bindings/wysiwyg-wasm/package.json 33 | 34 | echo "Updating Android" 35 | $SED_CMD "$GRADLE_PROPERTIES_REGEX" $SCRIPT_PATH/platforms/android/gradle.properties 36 | --------------------------------------------------------------------------------