├── .buildkite ├── pipeline.yml └── shared-pipeline-vars ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .java-version ├── .nvmrc ├── .prettierignore ├── .prettierrc.cjs ├── .xcode-version ├── Makefile ├── Package.swift ├── README.md ├── __mocks__ └── @wordpress │ ├── components │ └── index.jsx │ ├── hooks.js │ └── i18n │ └── index.js ├── android ├── Gutenberg │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── org │ │ │ └── wordpress │ │ │ └── gutenberg │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── org │ │ │ └── wordpress │ │ │ └── gutenberg │ │ │ ├── EditorConfiguration.kt │ │ │ ├── GutenbergJsException.kt │ │ │ ├── GutenbergRequestInterceptor.kt │ │ │ └── GutenbergView.kt │ │ └── test │ │ └── java │ │ └── org │ │ └── wordpress │ │ └── gutenberg │ │ ├── EditorConfigurationTest.kt │ │ ├── ExampleUnitTest.kt │ │ └── GutenbergViewTest.kt ├── app │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── gutenbergkit │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── gutenbergkit │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── activity_main.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-night │ │ │ └── themes.xml │ │ │ ├── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ │ └── xml │ │ │ ├── backup_rules.xml │ │ │ ├── data_extraction_rules.xml │ │ │ └── network_security_config.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── example │ │ └── gutenbergkit │ │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle │ ├── libs.versions.toml │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ├── bin └── prep-translations.js ├── docs ├── example-xcode-env-variable.png ├── gutenberg-kit-preview.png └── test-cases.md ├── ios ├── Demo-iOS │ ├── Gutenberg.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Gutenberg.xcscheme │ ├── PreviewContent │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── Resources │ │ └── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ │ └── Contents.json │ └── Sources │ │ ├── ContentView.swift │ │ ├── EditorView.swift │ │ └── GutenbergApp.swift ├── Sources │ └── GutenbergKit │ │ ├── Gutenberg │ │ ├── assets │ │ │ ├── ar-CyYCT0Yn.js │ │ │ ├── bg-B2djTlFt.js │ │ │ ├── bo-DOe3GxAO.js │ │ │ ├── bridge-COXq4kB3.js │ │ │ ├── ca--D3R8toT.js │ │ │ ├── cs-DWmcEIfO.js │ │ │ ├── cy-CGIlBSk6.js │ │ │ ├── da-DwB1fLza.js │ │ │ ├── de-H01QIQZg.js │ │ │ ├── editor-BBJZD2BL.css │ │ │ ├── editor-C-URa0vU.js │ │ │ ├── editor-DWxDd151.js │ │ │ ├── el-DhdK7rMw.js │ │ │ ├── en-au-CPC6XjFj.js │ │ │ ├── en-ca-VNTKwdAC.js │ │ │ ├── en-gb-DURVrT_X.js │ │ │ ├── en-nz-Dm2aq_Nd.js │ │ │ ├── en-za-BYpQIJT9.js │ │ │ ├── es-BJ3q4Wbp.js │ │ │ ├── es-ar-DaruD1vk.js │ │ │ ├── es-cl-BnB-IkOa.js │ │ │ ├── es-cr-BY95D2Di.js │ │ │ ├── fa-ByB6Bbvh.js │ │ │ ├── fr-HxuvMa7s.js │ │ │ ├── gl-McDu_gir.js │ │ │ ├── he-4TZWuuaG.js │ │ │ ├── hr-DOZGX9Ap.js │ │ │ ├── hu-BCFI5Ug4.js │ │ │ ├── id-BlpIZgTK.js │ │ │ ├── index-Cz5GNwXH.js │ │ │ ├── index-OGmBC7ij.css │ │ │ ├── is-u59YF_6S.js │ │ │ ├── it-OacWAa4D.js │ │ │ ├── ja-eaKkqtgl.js │ │ │ ├── ka-S_it2R8x.js │ │ │ ├── ko-lhFU0drW.js │ │ │ ├── localization-C-KsbNHa.js │ │ │ ├── logger-B2MeKcsF.js │ │ │ ├── nb-B4NppxVr.js │ │ │ ├── nl-DjtAcROr.js │ │ │ ├── nl-be-BLey3_eV.js │ │ │ ├── pl-BcZoHphp.js │ │ │ ├── pt-Pa9447RD.js │ │ │ ├── pt-br-B3s-WxNp.js │ │ │ ├── remote-3JsY0MSr.js │ │ │ ├── remote-CQD0zuAs.css │ │ │ ├── ro-D4eP2fbK.js │ │ │ ├── ru-DgpwNl7b.js │ │ │ ├── sk-C1b2igZL.js │ │ │ ├── sq-CnDv6A4t.js │ │ │ ├── sr-CtYHP_-W.js │ │ │ ├── sv-Ffrez4Bf.js │ │ │ ├── th-SkpHtVrn.js │ │ │ ├── tr-By6oiZsW.js │ │ │ ├── uk-TWibncG9.js │ │ │ ├── ur-CSeJ9e2h.js │ │ │ ├── vi-BO1ox2QA.js │ │ │ ├── vite-helpers-C1_kvBzA.js │ │ │ ├── zh-cn-B9xnqhUX.js │ │ │ └── zh-tw-DNHfmYCf.js │ │ ├── index.html │ │ └── remote.html │ │ └── Sources │ │ ├── AnyDecodable.swift │ │ ├── EditorBlock.swift │ │ ├── EditorBlockPicker.swift │ │ ├── EditorConfiguration.swift │ │ ├── EditorJSMessage.swift │ │ ├── EditorNetworking.swift │ │ ├── EditorService.swift │ │ ├── EditorTypes.swift │ │ ├── EditorViewController.swift │ │ ├── EditorViewControllerDelegate.swift │ │ └── GBWebView.swift └── Tests │ └── GutenbergKitTests │ ├── EditorConfigurationTests.swift │ └── GutenbergKitTests.swift ├── package-lock.json ├── package.json ├── patches ├── @wordpress+block-editor+14.17.0.patch ├── @wordpress+components+29.8.0.patch ├── @wordpress+rich-text+7.22.0.patch └── README.md ├── src ├── components │ ├── default-block-appender │ │ ├── index.jsx │ │ └── style.scss │ ├── editor-load-error │ │ ├── index.jsx │ │ └── style.scss │ ├── editor-load-notice.jsx │ ├── editor-load-notice.test.jsx │ ├── editor-toolbar │ │ ├── aria-helper.js │ │ ├── index.jsx │ │ ├── style.scss │ │ └── use-modalize.js │ ├── editor │ │ ├── index.jsx │ │ ├── style.scss │ │ ├── test │ │ │ └── use-media-upload.test.jsx │ │ ├── use-editor-setup.js │ │ ├── use-host-bridge.js │ │ ├── use-host-exception-logging.js │ │ ├── use-media-upload.js │ │ └── use-sync-history-controls.js │ ├── layout │ │ ├── index.jsx │ │ └── style.scss │ ├── text-editor │ │ ├── index.jsx │ │ └── style.scss │ └── visual-editor │ │ ├── default-theme-styles.scss │ │ ├── index.jsx │ │ ├── style.scss │ │ ├── use-editor-styles.js │ │ ├── use-editor-visible.js │ │ └── wp-common-styles.scss ├── index.html ├── index.jsx ├── index.scss ├── lock-unlock.js ├── remote.html ├── remote.jsx └── utils │ ├── api-fetch.js │ ├── blocks.js │ ├── bridge.js │ ├── editor.jsx │ ├── exception-parser.js │ ├── localization.js │ ├── logger.js │ ├── post-type-entities.js │ ├── remote-editor.js │ └── stack-parsers.js ├── vite.config.js ├── vite.config.remote.js ├── vitest.config.js └── vitest.setup.js /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | agents: 2 | queue: mac 3 | 4 | env: 5 | IMAGE_ID: $IMAGE_ID 6 | 7 | steps: 8 | - label: ':react: Build React App' 9 | command: make build 10 | plugins: &plugins 11 | - $CI_TOOLKIT_PLUGIN 12 | - $NVM_PLUGIN 13 | 14 | - label: ':eslint: Lint React App' 15 | command: make lint-js 16 | plugins: *plugins 17 | 18 | - label: ':javascript: Test JavaScript' 19 | command: make test-js 20 | plugins: *plugins 21 | 22 | - label: ':android: Publish Android Library' 23 | command: | 24 | make build 25 | echo "--- :android: Publishing Android Library" 26 | ./android/gradlew -p ./android :gutenberg:prepareToPublishToS3 $(prepare_to_publish_to_s3_params) :gutenberg:publish 27 | agents: 28 | queue: android 29 | plugins: *plugins 30 | 31 | - label: ':android: Test Android Library' 32 | command: make test-android 33 | agents: 34 | queue: android 35 | plugins: *plugins 36 | 37 | - label: ':swift: Test Swift Package' 38 | command: make test-swift-package 39 | plugins: *plugins 40 | -------------------------------------------------------------------------------- /.buildkite/shared-pipeline-vars: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This file is `source`'d before calling `buildkite-agent pipeline upload`, and can be used 4 | # to set up some variables that will be interpolated in the `.yml` pipeline before uploading it. 5 | 6 | # The ~> modifier is not currently used, but we check for it just in case 7 | XCODE_VERSION=$(sed 's/^~> *//' .xcode-version) 8 | CI_TOOLKIT_PLUGIN_VERSION="3.5.1" 9 | NVM_PLUGIN_VERSION='0.3.0' 10 | 11 | export IMAGE_ID="xcode-$XCODE_VERSION" 12 | export CI_TOOLKIT_PLUGIN="automattic/a8c-ci-toolkit#$CI_TOOLKIT_PLUGIN_VERSION" 13 | export NVM_PLUGIN="automattic/nvm#$NVM_PLUGIN_VERSION" 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ], 4 | env: { 5 | browser: true, 6 | es2020: true, 7 | }, 8 | ignorePatterns: [ 'android', 'dist', 'ios' ], 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module', 12 | }, 13 | plugins: [ 'react-refresh' ], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug with the WordPress block editor or Gutenberg plugin 3 | type: Bug 4 | labels: ['[Type] Bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! If this is a security issue, please report it in HackerOne instead: https://hackerone.com/wordpress 10 | - type: textarea 11 | attributes: 12 | label: Description 13 | description: Please write a brief description of the bug, including what you expect to happen and what is currently happening. 14 | placeholder: | 15 | Feature '...' is not working properly. I expect '...' to happen, but '...' happens instead 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | attributes: 21 | label: Step-by-step reproduction instructions 22 | description: Please write the steps needed to reproduce the bug. 23 | placeholder: | 24 | 1. Go to '...' 25 | 2. Click on '...' 26 | 3. Scroll down to '...' 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | attributes: 32 | label: Screenshots, screen recording, code snippet 33 | description: | 34 | If possible, please upload a screenshot or screen recording which demonstrates the bug. You can use LIEcap to create a GIF screen recording: https://www.cockos.com/licecap/ 35 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 36 | If this bug is to related to a developer API, please share a code snippet that demonstrates the issue. For small snippets paste it directly here, or you can use GitHub Gist to share multiple code files: https://gist.github.com 37 | Please ensure the shared code can be used by a developer to reproduce the issue—ideally it can be copied into a local development environment or executed in a browser console to help debug the issue 38 | validations: 39 | required: false 40 | 41 | - type: textarea 42 | attributes: 43 | label: Environment info 44 | description: | 45 | Please list what Gutenberg version you are using. If you aren't using Gutenberg, please note that it's not installed. 46 | placeholder: | 47 | - WordPress version, Gutenberg version, and active Theme you are using. 48 | - Browser(s) are you seeing the problem on. 49 | - Device you are using and operating system (e.g. "Desktop with Windows 10", "iPhone with iOS 14", etc.). 50 | validations: 51 | required: false 52 | 53 | - type: checkboxes 54 | id: existing 55 | attributes: 56 | label: Please confirm that you have searched existing issues in the repo. 57 | description: You can do this by searching https://github.com/WordPress/gutenberg/issues and making sure the bug is not related to another plugin. 58 | options: 59 | - label: 'Yes' 60 | required: true 61 | 62 | - type: checkboxes 63 | id: plugins 64 | attributes: 65 | label: Please confirm that you have tested with all plugins deactivated except Gutenberg. 66 | options: 67 | - label: 'Yes' 68 | required: true 69 | 70 | - type: checkboxes 71 | id: themes 72 | attributes: 73 | label: Please confirm which theme type you used for testing. 74 | options: 75 | - label: 'Block' 76 | - label: 'Classic' 77 | - label: 'Hybrid (e.g. classic with theme.json)' 78 | - label: 'Not sure' 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Propose an idea for a feature or an enhancement 4 | type: Feature 5 | labels: "[Type] Enhancement" 6 | --- 7 | 8 | ## What problem does this address? 9 | 14 | 15 | ## What is your proposed solution? 16 | 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | ## What? 5 | 6 | 7 | ## Why? 8 | 9 | 10 | ## How? 11 | 12 | 13 | ## Testing Instructions 14 | 15 | 16 | 17 | 18 | 19 | ### Accessibility Testing Instructions 20 | 21 | 22 | ## Screenshots or screencast 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## System 2 | .DS_Store 3 | 4 | ## Build generated 5 | build/ 6 | DerivedData 7 | .swiftpm/ 8 | UserInterfaceState.xcuserstate 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata 20 | 21 | ## Other 22 | *.xccheckout 23 | *.moved-aside 24 | *.xcuserstate 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | 36 | ## Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | 41 | .build/ 42 | .swiftpm 43 | 44 | # Logs 45 | logs 46 | *.log 47 | npm-debug.log* 48 | yarn-debug.log* 49 | yarn-error.log* 50 | lerna-debug.log* 51 | .pnpm-debug.log* 52 | 53 | # Diagnostic reports (https://nodejs.org/api/report.html) 54 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 55 | 56 | # Runtime data 57 | pids 58 | *.pid 59 | *.seed 60 | *.pid.lock 61 | 62 | # Directory for instrumented libs generated by jscoverage/JSCover 63 | lib-cov 64 | 65 | # Coverage directory used by tools like istanbul 66 | coverage 67 | *.lcov 68 | 69 | # nyc test coverage 70 | .nyc_output 71 | 72 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 73 | .grunt 74 | 75 | # Bower dependency directory (https://bower.io/) 76 | bower_components 77 | 78 | # node-waf configuration 79 | .lock-wscript 80 | 81 | # Dependency directories 82 | node_modules/ 83 | jspm_packages/ 84 | 85 | # Snowpack dependency directory (https://snowpack.dev/) 86 | web_modules/ 87 | 88 | # TypeScript cache 89 | *.tsbuildinfo 90 | 91 | # Optional npm cache directory 92 | .npm 93 | 94 | # Optional eslint cache 95 | .eslintcache 96 | 97 | # Optional stylelint cache 98 | .stylelintcache 99 | 100 | # Microbundle cache 101 | .rpt2_cache/ 102 | .rts2_cache_cjs/ 103 | .rts2_cache_es/ 104 | .rts2_cache_umd/ 105 | 106 | # Optional REPL history 107 | .node_repl_history 108 | 109 | # Output of 'npm pack' 110 | *.tgz 111 | 112 | # Yarn Integrity file 113 | .yarn-integrity 114 | 115 | # dotenv environment variable files 116 | .env 117 | .env.development.local 118 | .env.test.local 119 | .env.production.local 120 | .env.local 121 | 122 | # parcel-bundler cache (https://parceljs.org/) 123 | .cache 124 | .parcel-cache 125 | 126 | # Next.js build output 127 | .next 128 | out 129 | 130 | # Nuxt.js build / generate output 131 | .nuxt 132 | dist 133 | 134 | # Gatsby files 135 | .cache/ 136 | # Comment in the public line in if your project uses Gatsby and not Next.js 137 | # https://nextjs.org/blog/next-9-1#public-directory-support 138 | # public 139 | 140 | # vuepress build output 141 | .vuepress/dist 142 | 143 | # vuepress v2.x temp and cache directory 144 | .temp 145 | .cache 146 | 147 | # Docusaurus cache and generated files 148 | .docusaurus 149 | 150 | # Serverless directories 151 | .serverless/ 152 | 153 | # FuseBox cache 154 | .fusebox/ 155 | 156 | # DynamoDB Local files 157 | .dynamodb/ 158 | 159 | # TernJS port file 160 | .tern-port 161 | 162 | # Editor directories and files 163 | .vscode/* 164 | !.vscode/extensions.json 165 | .vscode-test 166 | .idea 167 | .DS_Store 168 | *.suo 169 | *.ntvs* 170 | *.njsproj 171 | *.sln 172 | *.sw? 173 | 174 | # yarn v2 175 | .yarn/cache 176 | .yarn/unplugged 177 | .yarn/build-state.yml 178 | .yarn/install-state.gz 179 | .pnp.* 180 | 181 | # Android App 182 | local.properties 183 | /android/build 184 | /android/.gradle 185 | /android/.idea 186 | 187 | ## Production Build Products 188 | /android/Gutenberg/src/main/assets/assets 189 | /android/Gutenberg/src/main/assets/index.html 190 | /android/Gutenberg/src/main/assets/remote.html 191 | 192 | # Disabled removing these files until this is published like Android in CI. 193 | # /ios/Sources/GutenbergKit/Gutenberg/assets 194 | # /ios/Sources/GutenbergKit/Gutenberg/index.html 195 | # /ios/Sources/GutenbergKit/Gutenberg/remote.html 196 | 197 | # Translation files 198 | src/translations/ 199 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17.0 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | android 2 | ios 3 | package-lock.json 4 | .github/**/*.md 5 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require( '@wordpress/prettier-config' ); 2 | -------------------------------------------------------------------------------- /.xcode-version: -------------------------------------------------------------------------------- 1 | 15.4 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SIMULATOR_DESTINATION := OS=17.5,name=iPhone 15 Plus 2 | 3 | define XCODEBUILD_CMD 4 | @set -o pipefail && \ 5 | xcodebuild $(1) \ 6 | -scheme GutenbergKit \ 7 | -sdk iphonesimulator \ 8 | -destination '${SIMULATOR_DESTINATION}' \ 9 | | xcbeautify 10 | endef 11 | 12 | npm-dependencies: 13 | @if [ "$(SKIP_DEPS)" != "true" ] && [ "$(SKIP_DEPS)" != "1" ]; then \ 14 | echo "--- :npm: Installing NPM Dependencies"; \ 15 | npm ci; \ 16 | fi 17 | 18 | prep-translations: 19 | @if [ "$(SKIP_L10N)" != "true" ] && [ "$(SKIP_L10N)" != "1" ]; then \ 20 | echo "--- :npm: Preparing Translations"; \ 21 | npm run prep-translations -- --force; \ 22 | fi 23 | 24 | build: npm-dependencies prep-translations 25 | echo "--- :node: Building Gutenberg" 26 | 27 | npm run build 28 | 29 | # Copy build products into place 30 | echo "--- :open_file_folder: Copying Build Products into place" 31 | rm -rf ./ios/Sources/GutenbergKit/Gutenberg/ ./android/Gutenberg/src/main/assets/ 32 | cp -r ./dist/. ./ios/Sources/GutenbergKit/Gutenberg/ 33 | cp -r ./dist/. ./android/Gutenberg/src/main/assets 34 | 35 | dev-server: npm-dependencies 36 | npm run dev 37 | 38 | dev-server-remote: npm-dependencies 39 | npm run dev:remote 40 | 41 | fmt-js: npm-dependencies 42 | npm run format 43 | 44 | lint-js: npm-dependencies 45 | npm run lint 46 | 47 | test-js: npm-dependencies 48 | npm run test -- run 49 | 50 | local-android-library: build 51 | echo "--- :android: Building Library" 52 | ./android/gradlew -p ./android :gutenberg:publishToMavenLocal -exclude-task prepareToPublishToS3 53 | 54 | test-android: 55 | echo "--- :android: Running Android Tests" 56 | ./android/gradlew -p ./android :gutenberg:test 57 | 58 | build-swift-package: build 59 | $(call XCODEBUILD_CMD, build) 60 | 61 | test-swift-package: build 62 | $(call XCODEBUILD_CMD, test) 63 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "GutenbergKit", 8 | platforms: [.iOS(.v15), .macOS(.v14)], 9 | products: [ 10 | .library(name: "GutenbergKit", targets: ["GutenbergKit"]) 11 | ], 12 | targets: [ 13 | .target( 14 | name: "GutenbergKit", 15 | dependencies: [], 16 | path: "ios/Sources/GutenbergKit", 17 | exclude: [], 18 | resources: [.copy("Gutenberg")] 19 | ), 20 | .testTarget( 21 | name: "GutenbergKitTests", 22 | dependencies: ["GutenbergKit"], 23 | path: "ios/Tests", 24 | exclude: [] 25 | ) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GutenbergKit 2 | 3 | An experimental Gutenberg block editor for native iOS and Android apps relying upon web technologies. 4 | 5 | GutenbergKit running on an iPhone 6 | 7 | ## Development 8 | 9 | ### Preqrequisites 10 | 11 | In order to build GutenbergKit, the following tools must be installed on your development machine: 12 | 13 | - [Node.js](https://nodejs.org/en/download/) - Required for building the web app; recommend using [Node Version Manager](https://github.com/nvm-sh/nvm). 14 | - [Xcode](https://developer.apple.com/xcode/) - Required if building iOS demo app. 15 | - [Android Studio](https://developer.android.com/studio) - Required if building Android demo app. 16 | 17 | ### Web App 18 | 19 | Install the GutenbergKit dependencies and start the development server by running the following command in your terminal: 20 | 21 | ```bash 22 | make dev-server 23 | ``` 24 | 25 | Once finished, the web app can now be accessed in your browser by visiting the URL logged in your terminal. However, it is recommended to use a native host app for testing the changes made to the editor for a more realistic experience. A demo app is included in the GutenbergKit package, along with instructions on how to use it below. 26 | 27 | ### Demo App 28 | 29 | This demo app is useful for quickly testing changes made to the editor. By default, the demo app uses a production build of the web app bundled with the GutenbergKit package—i.e., the output of the project's `make build` command. During development, however, it is more useful to run the web app with a server and provide the server URL as an environment variable for the demo app, so that changes are displayed in the app immediately. 30 | 31 | #### iOS 32 | 33 | 1. Start the development server by running `make dev-server`. 34 | 1. Launch Xcode and open the `ios/Demo-iOS/Gutenberg.xcodeproj` project. 35 | 1. Select the `Gutenberg` target. 36 | 1. Navigate to _Product_ → _Scheme_ → _Edit Scheme_. 37 | 1. Add an environment variable named `GUTENBERG_EDITOR_URL` with the URL of the development server. 38 | 1. Run the app. 39 | 40 |
41 | Example Xcode environment variable 42 | 43 | Example Xcode environment variable 44 | 45 |
46 | 47 | #### Android 48 | 49 | 1. Start the development server by running `make dev-server`. 50 | 1. Launch Android Studio and open the `android` project. 51 | 1. Modify the `android/local.properties` file to include an environment variable named `GUTENBERG_EDITOR_URL` with the URL of the development server. 52 | 1. Run the app. 53 | 54 |
55 | Example Android local.properties 56 | 57 | ``` 58 | GUTENBERG_EDITOR_URL=http://:5173/ 59 | ``` 60 | 61 |
62 | 63 | ## Testing 64 | 65 | To run the JavaScript tests, run the following command in your terminal: 66 | 67 | ```bash 68 | make test-js 69 | ``` 70 | 71 | To run the Swift tests, run the following command in your terminal: 72 | 73 | ```bash 74 | make test-swift 75 | ``` 76 | 77 | To run the Android tests, run the following command in your terminal: 78 | 79 | ```bash 80 | make test-android 81 | ``` 82 | 83 | ## Production 84 | 85 | To build GutenbergKit for production run the following command in your terminal: 86 | 87 | ```bash 88 | make build 89 | ``` 90 | 91 | Once finished, the Swift and Kotlin packages are ready to publish. Consuming iOS or Android host apps can then include the GutenbergKit package as a dependency. 92 | -------------------------------------------------------------------------------- /__mocks__/@wordpress/components/index.jsx: -------------------------------------------------------------------------------- 1 | export const Notice = ( { children, actions } ) => ( 2 |
3 | { children } 4 | { actions?.map( ( action, index ) => ( 5 | 12 | ) ) } 13 |
14 | ); 15 | -------------------------------------------------------------------------------- /__mocks__/@wordpress/hooks.js: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | export const addFilter = vi.fn(); 4 | export const removeFilter = vi.fn(); 5 | -------------------------------------------------------------------------------- /__mocks__/@wordpress/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | export const __ = vi.fn( ( text ) => text ); 4 | -------------------------------------------------------------------------------- /android/Gutenberg/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android/Gutenberg/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | id("com.automattic.android.publish-to-s3") 5 | id("kotlin-parcelize") 6 | } 7 | 8 | android { 9 | namespace = "org.wordpress.gutenberg" 10 | compileSdk = 34 11 | 12 | buildFeatures { 13 | buildConfig = true 14 | } 15 | 16 | defaultConfig { 17 | minSdk = 22 18 | 19 | buildConfigField( 20 | "String", 21 | "GUTENBERG_EDITOR_URL", 22 | "\"${rootProject.ext["gutenbergEditorUrl"] ?: ""}\"" 23 | ) 24 | 25 | buildConfigField( 26 | "String", 27 | "GUTENBERG_EDITOR_REMOTE_URL", 28 | "\"${rootProject.ext["gutenbergEditorRemoteUrl"] ?: ""}\"" 29 | ) 30 | 31 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 32 | consumerProguardFiles("consumer-rules.pro") 33 | } 34 | 35 | buildTypes { 36 | release { 37 | isMinifyEnabled = false 38 | proguardFiles( 39 | getDefaultProguardFile("proguard-android-optimize.txt"), 40 | "proguard-rules.pro" 41 | ) 42 | } 43 | } 44 | compileOptions { 45 | sourceCompatibility = JavaVersion.VERSION_1_8 46 | targetCompatibility = JavaVersion.VERSION_1_8 47 | } 48 | kotlinOptions { 49 | jvmTarget = "1.8" 50 | } 51 | } 52 | 53 | dependencies { 54 | 55 | implementation(libs.androidx.core.ktx) 56 | implementation(libs.androidx.appcompat) 57 | implementation(libs.material) 58 | implementation(libs.androidx.webkit) 59 | implementation(libs.gson) 60 | 61 | testImplementation(libs.junit) 62 | testImplementation(libs.mockito.core) 63 | testImplementation(libs.mockito.kotlin) 64 | testImplementation(libs.robolectric) 65 | androidTestImplementation(libs.androidx.junit) 66 | androidTestImplementation(libs.androidx.espresso.core) 67 | } 68 | 69 | project.afterEvaluate { 70 | publishing { 71 | publications { 72 | create("maven") { 73 | from(components["release"]) 74 | 75 | groupId = "org.wordpress.gutenbergkit" 76 | artifactId = "android" 77 | // version is set by 'publish-to-s3' plugin 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /android/Gutenberg/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/Gutenberg/consumer-rules.pro -------------------------------------------------------------------------------- /android/Gutenberg/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /android/Gutenberg/src/androidTest/java/org/wordpress/gutenberg/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.gutenberg 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("org.wordpress.gutenberg.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /android/Gutenberg/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergJsException.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.gutenberg 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.JsonObject 5 | 6 | data class JsExceptionStackTraceElement ( 7 | val fileName: String?, 8 | val lineNumber: Int?, 9 | val colNumber: Int?, 10 | val function: String, 11 | ) 12 | class GutenbergJsException ( 13 | val type: String, 14 | val message: String, 15 | var stackTrace: List, 16 | val context: Map = emptyMap(), 17 | val tags: Map = emptyMap(), 18 | val isHandled: Boolean, 19 | val handledBy: String 20 | ) { 21 | 22 | companion object { 23 | @JvmStatic 24 | fun fromString(exceptionString: String): GutenbergJsException { 25 | val gson = Gson() 26 | val rawException = gson.fromJson(exceptionString, JsonObject::class.java) 27 | 28 | val type = rawException.get("type")?.asString ?: "" 29 | val message = rawException.get("message")?.asString ?: "" 30 | 31 | val stackTrace = rawException.getAsJsonArray("stacktrace")?.map { element -> 32 | val stackTraceElement = element.asJsonObject 33 | val stackTraceFunction = stackTraceElement.get("function")?.asString 34 | stackTraceFunction?.let { 35 | JsExceptionStackTraceElement( 36 | stackTraceElement.get("filename")?.asString, 37 | stackTraceElement.get("lineno")?.asInt, 38 | stackTraceElement.get("colno")?.asInt, 39 | stackTraceFunction 40 | ) 41 | } 42 | }?.filterNotNull() ?: emptyList() 43 | 44 | val context = rawException.getAsJsonObject("context")?.entrySet()?.associate { 45 | it.key to it.value.asString 46 | } ?: emptyMap() 47 | 48 | val tags = rawException.getAsJsonObject("tags")?.entrySet()?.associate { 49 | it.key to it.value.asString 50 | } ?: emptyMap() 51 | 52 | val isHandled = rawException.get("isHandled")?.asBoolean ?: false 53 | val handledBy = rawException.get("handledBy")?.asString ?: "" 54 | 55 | return GutenbergJsException( 56 | type, 57 | message, 58 | stackTrace, 59 | context, 60 | tags, 61 | isHandled, 62 | handledBy 63 | ) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /android/Gutenberg/src/main/java/org/wordpress/gutenberg/GutenbergRequestInterceptor.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.gutenberg 2 | 3 | import android.webkit.WebResourceRequest 4 | import android.webkit.WebResourceResponse 5 | 6 | public interface GutenbergRequestInterceptor { 7 | fun canIntercept(request: WebResourceRequest): Boolean 8 | fun handleRequest(request: WebResourceRequest): WebResourceResponse? 9 | } 10 | 11 | class DefaultGutenbergRequestInterceptor: GutenbergRequestInterceptor { 12 | override fun canIntercept(request: WebResourceRequest): Boolean { 13 | return false 14 | } 15 | 16 | override fun handleRequest(request: WebResourceRequest): WebResourceResponse? { 17 | return null 18 | } 19 | } -------------------------------------------------------------------------------- /android/Gutenberg/src/test/java/org/wordpress/gutenberg/EditorConfigurationTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.gutenberg 2 | 3 | import org.junit.Test 4 | import org.junit.Assert.* 5 | import org.junit.Before 6 | 7 | class EditorConfigurationTest { 8 | private lateinit var editorConfig: EditorConfiguration 9 | 10 | @Before 11 | fun setup() { 12 | editorConfig = EditorConfiguration.builder() 13 | .setTitle("Test Title") 14 | .setContent("Test Content") 15 | .setPostId(123) 16 | .setPostType("post") 17 | .setThemeStyles(true) 18 | .setPlugins(true) 19 | .setHideTitle(false) 20 | .setSiteURL("https://example.com") 21 | .setSiteApiRoot("https://example.com/wp-json") 22 | .setSiteApiNamespace(arrayOf("wp/v2")) 23 | .setNamespaceExcludedPaths(arrayOf("users")) 24 | .setAuthHeader("Bearer token") 25 | .setWebViewGlobals(listOf( 26 | WebViewGlobal("testString", WebViewGlobalValue.StringValue("test")), 27 | WebViewGlobal("testNumber", WebViewGlobalValue.NumberValue(42.0)), 28 | WebViewGlobal("testBoolean", WebViewGlobalValue.BooleanValue(true)) 29 | )) 30 | .build() 31 | } 32 | 33 | @Test 34 | fun `test EditorConfiguration builder creates correct configuration`() { 35 | assertEquals("Test Title", editorConfig.title) 36 | assertEquals("Test Content", editorConfig.content) 37 | assertEquals(123, editorConfig.postId) 38 | assertEquals("post", editorConfig.postType) 39 | assertTrue(editorConfig.themeStyles) 40 | assertTrue(editorConfig.plugins) 41 | assertFalse(editorConfig.hideTitle) 42 | assertEquals("https://example.com", editorConfig.siteURL) 43 | assertEquals("https://example.com/wp-json", editorConfig.siteApiRoot) 44 | assertArrayEquals(arrayOf("wp/v2"), editorConfig.siteApiNamespace) 45 | assertArrayEquals(arrayOf("users"), editorConfig.namespaceExcludedPaths) 46 | assertEquals("Bearer token", editorConfig.authHeader) 47 | assertEquals(3, editorConfig.webViewGlobals.size) 48 | } 49 | 50 | @Test 51 | fun `test WebViewGlobal StringValue toJavaScript conversion`() { 52 | val stringValue = WebViewGlobalValue.StringValue("test\nvalue") 53 | assertEquals("\"test\\nvalue\"", stringValue.toJavaScript()) 54 | } 55 | 56 | @Test 57 | fun `test WebViewGlobal NumberValue toJavaScript conversion`() { 58 | val numberValue = WebViewGlobalValue.NumberValue(42.0) 59 | assertEquals("42.0", numberValue.toJavaScript()) 60 | } 61 | 62 | @Test 63 | fun `test WebViewGlobal BooleanValue toJavaScript conversion`() { 64 | val booleanValue = WebViewGlobalValue.BooleanValue(true) 65 | assertEquals("true", booleanValue.toJavaScript()) 66 | } 67 | 68 | @Test 69 | fun `test WebViewGlobal ObjectValue toJavaScript conversion`() { 70 | val objectValue = WebViewGlobalValue.ObjectValue(mapOf( 71 | "key1" to WebViewGlobalValue.StringValue("value1"), 72 | "key2" to WebViewGlobalValue.NumberValue(42.0) 73 | )) 74 | assertEquals("{\"key1\": \"value1\",\"key2\": 42.0}", objectValue.toJavaScript()) 75 | } 76 | 77 | @Test 78 | fun `test WebViewGlobal ArrayValue toJavaScript conversion`() { 79 | val arrayValue = WebViewGlobalValue.ArrayValue(listOf( 80 | WebViewGlobalValue.StringValue("value1"), 81 | WebViewGlobalValue.NumberValue(42.0) 82 | )) 83 | assertEquals("[\"value1\",42.0]", arrayValue.toJavaScript()) 84 | } 85 | 86 | @Test 87 | fun `test WebViewGlobal NullValue toJavaScript conversion`() { 88 | val nullValue = WebViewGlobalValue.NullValue 89 | assertEquals("null", nullValue.toJavaScript()) 90 | } 91 | 92 | @Test 93 | fun `test WebViewGlobal valid identifier`() { 94 | val validGlobal = WebViewGlobal("validName", WebViewGlobalValue.StringValue("test")) 95 | assertEquals("validName", validGlobal.name) 96 | } 97 | 98 | @Test(expected = IllegalArgumentException::class) 99 | fun `test WebViewGlobal invalid identifier throws exception`() { 100 | WebViewGlobal("123invalid", WebViewGlobalValue.StringValue("test")) 101 | } 102 | 103 | @Test 104 | fun `test EditorConfiguration equals and hashCode`() { 105 | val config1 = EditorConfiguration.builder() 106 | .setTitle("Test") 107 | .setContent("Content") 108 | .build() 109 | 110 | val config2 = EditorConfiguration.builder() 111 | .setTitle("Test") 112 | .setContent("Content") 113 | .build() 114 | 115 | assertEquals(config1, config2) 116 | assertEquals(config1.hashCode(), config2.hashCode()) 117 | } 118 | 119 | @Test 120 | fun `test EditorConfiguration not equals`() { 121 | val config1 = EditorConfiguration.builder() 122 | .setTitle("Test1") 123 | .setContent("Content") 124 | .build() 125 | 126 | val config2 = EditorConfiguration.builder() 127 | .setTitle("Test2") 128 | .setContent("Content") 129 | .build() 130 | 131 | assertNotEquals(config1, config2) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /android/Gutenberg/src/test/java/org/wordpress/gutenberg/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.gutenberg 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /android/Gutenberg/src/test/java/org/wordpress/gutenberg/GutenbergViewTest.kt: -------------------------------------------------------------------------------- 1 | package org.wordpress.gutenberg 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Looper 6 | import android.webkit.ValueCallback 7 | import android.webkit.WebChromeClient 8 | import android.webkit.WebView 9 | import org.junit.Before 10 | import org.junit.Test 11 | import org.junit.runner.RunWith 12 | import org.mockito.Mock 13 | import org.mockito.Mockito.`when` 14 | import org.mockito.MockitoAnnotations 15 | import org.robolectric.RobolectricTestRunner 16 | import org.robolectric.RuntimeEnvironment 17 | import org.robolectric.Shadows.shadowOf 18 | import org.robolectric.annotation.Config 19 | import org.junit.Assert.assertEquals 20 | import org.junit.Assert.assertTrue 21 | import java.util.concurrent.CountDownLatch 22 | import java.util.concurrent.TimeUnit 23 | 24 | @RunWith(RobolectricTestRunner::class) 25 | @Config(sdk = [28], manifest = Config.NONE) 26 | class GutenbergViewTest { 27 | @Mock 28 | private lateinit var mockWebView: WebView 29 | 30 | @Mock 31 | private lateinit var mockFilePathCallback: ValueCallback?> 32 | 33 | @Mock 34 | private lateinit var mockFileChooserParams: WebChromeClient.FileChooserParams 35 | 36 | private lateinit var gutenbergView: GutenbergView 37 | 38 | @Before 39 | fun setup() { 40 | MockitoAnnotations.openMocks(this) 41 | gutenbergView = GutenbergView(RuntimeEnvironment.getApplication()) 42 | gutenbergView.initializeWebView() 43 | } 44 | 45 | @Test 46 | fun `onShowFileChooser sets up file chooser with single file selection`() { 47 | // Given 48 | val latch = CountDownLatch(1) 49 | var capturedIntent: Intent? = null 50 | 51 | gutenbergView.setOnFileChooserRequestedListener { intent, _ -> 52 | capturedIntent = intent 53 | latch.countDown() 54 | } 55 | 56 | // When 57 | gutenbergView.webChromeClient?.onShowFileChooser( 58 | mockWebView, 59 | mockFilePathCallback, 60 | mockFileChooserParams 61 | ) 62 | 63 | // Process any pending runnables 64 | shadowOf(Looper.getMainLooper()).runToEndOfTasks() 65 | 66 | // Wait for the callback to be executed 67 | latch.await(1, TimeUnit.SECONDS) 68 | 69 | // Then 70 | assertTrue("Intent should not be null", capturedIntent != null) 71 | assertTrue("Intent should be a chooser", capturedIntent?.action == Intent.ACTION_CHOOSER) 72 | 73 | // Get the original intent from the chooser 74 | val originalIntent = capturedIntent?.getParcelableExtra(Intent.EXTRA_INTENT) 75 | assertTrue("Original intent should not be null", originalIntent != null) 76 | assertTrue("Original intent action should be ACTION_GET_CONTENT", 77 | originalIntent?.action == Intent.ACTION_GET_CONTENT) 78 | assertTrue("Original intent should have CATEGORY_OPENABLE", 79 | originalIntent?.hasCategory(Intent.CATEGORY_OPENABLE) == true) 80 | assertEquals("Pick image request code should be 1", 81 | 1, gutenbergView.pickImageRequestCode) 82 | } 83 | 84 | @Test 85 | fun `onShowFileChooser sets up file chooser with multiple file selection`() { 86 | // Given 87 | val latch = CountDownLatch(1) 88 | var capturedIntent: Intent? = null 89 | 90 | gutenbergView.setOnFileChooserRequestedListener { intent, _ -> 91 | capturedIntent = intent 92 | latch.countDown() 93 | } 94 | 95 | // When 96 | `when`(mockFileChooserParams.mode).thenReturn(WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE) 97 | gutenbergView.webChromeClient?.onShowFileChooser( 98 | mockWebView, 99 | mockFilePathCallback, 100 | mockFileChooserParams 101 | ) 102 | 103 | // Process any pending runnables 104 | shadowOf(Looper.getMainLooper()).runToEndOfTasks() 105 | 106 | // Wait for the callback to be executed 107 | latch.await(1, TimeUnit.SECONDS) 108 | 109 | // Then 110 | assertTrue("Intent should not be null", capturedIntent != null) 111 | assertTrue("Intent should be a chooser", capturedIntent?.action == Intent.ACTION_CHOOSER) 112 | 113 | // Get the original intent from the chooser 114 | val originalIntent = capturedIntent?.getParcelableExtra(Intent.EXTRA_INTENT) 115 | assertTrue("Original intent should not be null", originalIntent != null) 116 | assertTrue("Original intent should allow multiple selection", 117 | originalIntent?.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) == true) 118 | } 119 | 120 | @Test 121 | fun `onShowFileChooser stores file path callback`() { 122 | // When 123 | gutenbergView.webChromeClient?.onShowFileChooser( 124 | mockWebView, 125 | mockFilePathCallback, 126 | mockFileChooserParams 127 | ) 128 | 129 | // Then 130 | assertEquals("File path callback should be stored", 131 | mockFilePathCallback, gutenbergView.filePathCallback) 132 | } 133 | 134 | @Test 135 | fun `resetFilePathCallback clears the callback`() { 136 | // Given 137 | gutenbergView.webChromeClient?.onShowFileChooser( 138 | mockWebView, 139 | mockFilePathCallback, 140 | mockFileChooserParams 141 | ) 142 | 143 | // When 144 | gutenbergView.resetFilePathCallback() 145 | 146 | // Then 147 | assertEquals("File path callback should be null after reset", 148 | null, gutenbergView.filePathCallback) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.jetbrains.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.example.gutenbergkit" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "com.example.gutenbergkit" 12 | minSdk = 22 13 | targetSdk = 34 14 | versionCode = 1 15 | versionName = "1.0" 16 | 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | isMinifyEnabled = false 23 | proguardFiles( 24 | getDefaultProguardFile("proguard-android-optimize.txt"), 25 | "proguard-rules.pro" 26 | ) 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | targetCompatibility = JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = "1.8" 35 | } 36 | } 37 | 38 | dependencies { 39 | 40 | implementation(libs.androidx.core.ktx) 41 | implementation(libs.androidx.appcompat) 42 | implementation(libs.material) 43 | implementation(libs.androidx.activity) 44 | implementation(libs.androidx.constraintlayout) 45 | implementation(libs.androidx.webkit) 46 | implementation(project(":Gutenberg")) 47 | testImplementation(libs.junit) 48 | androidTestImplementation(libs.androidx.junit) 49 | androidTestImplementation(libs.androidx.espresso.core) 50 | } -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /android/app/src/androidTest/java/com/example/gutenbergkit/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.gutenbergkit 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.gutenbergkit", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/gutenbergkit/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.gutenbergkit 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.webkit.WebView 6 | import androidx.activity.enableEdgeToEdge 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.view.ViewCompat 9 | import androidx.core.view.WindowInsetsCompat 10 | import org.wordpress.gutenberg.GutenbergView 11 | import org.wordpress.gutenberg.EditorConfiguration 12 | 13 | class MainActivity : AppCompatActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | enableEdgeToEdge() 18 | setContentView(R.layout.activity_main) 19 | ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> 20 | val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) 21 | v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) 22 | insets 23 | } 24 | 25 | WebView.setWebContentsDebuggingEnabled(true) 26 | 27 | val gbView = findViewById(R.id.gutenbergView) 28 | 29 | val config = EditorConfiguration.builder() 30 | .setTitle("") 31 | .setContent("") 32 | .setPostType("post") 33 | .setThemeStyles(false) 34 | .setPlugins(false) 35 | .setHideTitle(false) 36 | .setSiteURL("") 37 | .setSiteApiRoot("") 38 | .setSiteApiNamespace(arrayOf()) 39 | .setNamespaceExcludedPaths(arrayOf()) 40 | .setAuthHeader("") 41 | .setWebViewGlobals(emptyList()) 42 | .build() 43 | 44 | gbView.start(config) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/GutenbergKit/f26928ac3a15ad619dd4f6ebf02402e385edce6f/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF000000 4 | #FFFFFFFF 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GutenbergKit 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |