├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── e2e.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── examples.md ├── examples ├── examples-highlight.css ├── examples.css └── index.html ├── package.json ├── playwright.config.ts ├── src ├── config.js ├── discourse-preview-panel-handler.js ├── executable-code │ ├── exception.js │ ├── exception.monk │ ├── executable-fragment.js │ ├── executable-fragment.monk │ └── index.js ├── img │ ├── attention.svg │ ├── button_close.svg │ ├── button_close_darcula.svg │ ├── button_open.svg │ ├── button_open_darcula.svg │ ├── close-console-icon.svg │ ├── fail.svg │ ├── ok.svg │ ├── run.svg │ └── shorter.svg ├── index.js ├── js-executor │ ├── execute-es-module.js │ ├── index.js │ └── index.scss ├── lib │ └── crosslink.ts ├── styles.scss ├── utils │ ├── escape.js │ ├── index.js │ ├── platforms │ │ ├── TargetPlatform.ts │ │ ├── TargetPlatforms.ts │ │ └── index.ts │ └── types.ts ├── view │ ├── completion-view.js │ └── output-view.js └── webdemo-api.js ├── tests ├── __screenshots__ │ ├── dev │ │ └── Desktop-Chrome │ │ │ ├── basics.e2e.ts-basics-highlight-only-1.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-1.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-2.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-3.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-4.png │ │ │ ├── crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png │ │ │ ├── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png │ │ │ └── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png │ ├── github_linux │ │ ├── Desktop-Chrome │ │ │ ├── basics.e2e.ts-basics-highlight-only-1.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-1.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-2.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-3.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-4.png │ │ │ ├── crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png │ │ │ ├── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png │ │ │ └── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png │ │ └── Desktop-Firefox │ │ │ ├── basics.e2e.ts-basics-highlight-only-1.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-1.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-2.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-3.png │ │ │ ├── basics.e2e.ts-basics-simple-usage-4.png │ │ │ ├── crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png │ │ │ ├── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png │ │ │ └── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png │ └── github_mac │ │ └── Desktop-Safari │ │ ├── basics.e2e.ts-basics-highlight-only-1.png │ │ ├── basics.e2e.ts-basics-simple-usage-1.png │ │ ├── basics.e2e.ts-basics-simple-usage-2.png │ │ ├── basics.e2e.ts-basics-simple-usage-3.png │ │ ├── basics.e2e.ts-basics-simple-usage-4.png │ │ ├── crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png │ │ ├── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png │ │ └── wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png ├── basics.e2e.ts ├── crosslink.e2e.ts ├── crosslink.test.ts ├── min-version.test.ts ├── restrictions.e2e.ts ├── utils │ ├── index.ts │ ├── interactions.ts │ ├── mocks │ │ ├── compiler.ts │ │ ├── versions.json │ │ ├── wasm-1.9 │ │ │ ├── result.ts │ │ │ └── wasm.json │ │ └── wasm │ │ │ ├── result.ts │ │ │ └── wasm.json │ ├── screenshots.ts │ ├── selectors.ts │ └── server │ │ ├── index.js │ │ ├── playground.html │ │ └── playground.ts └── wasm-compatibility.e2e.ts ├── tsconfig.json ├── utils ├── copy-examples.js └── markdown-loader.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "useBuiltIns": "usage", 8 | "corejs": 2, 9 | "exclude": [ 10 | "transform-async-to-generator", 11 | "transform-regenerator" 12 | ] 13 | } 14 | ] 15 | ], 16 | "plugins": [ 17 | [ 18 | "@babel/plugin-transform-runtime", 19 | { 20 | "helpers": true, 21 | "corejs": 2, 22 | "regenerator": false 23 | } 24 | ], 25 | "module:fast-async", 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | IE 11 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /out 2 | /build 3 | /.idea 4 | /node_modules 5 | /dist 6 | /test-results/ 7 | /playwright-report/ 8 | /playwright/ 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:prettier/recommended', 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | createDefaultProgram: true, 14 | project: ['./tsconfig.json'], 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: 12, 19 | sourceType: 'module', 20 | }, 21 | plugins: ['@typescript-eslint', 'eslint-plugin-import'], 22 | rules: { 23 | quotes: ['error', 'single', { 24 | 'avoidEscape': true, 25 | 'allowTemplateLiterals': false 26 | }] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: "Tests: release bundle" 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | integration: 14 | name: Run end-to-end tests 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | TEST_PROJECT_LIST: github_linux 21 | - os: macos-latest 22 | TEST_PROJECT_LIST: github_mac 23 | 24 | runs-on: ${{ matrix.os }} 25 | timeout-minutes: 25 26 | 27 | steps: 28 | # prepare core binaries 29 | - uses: actions/checkout@v4 30 | 31 | - name: Install Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | cache: 'yarn' 36 | 37 | - name: Install dependencies 38 | run: yarn install --frozen-lockfile # optional, --immutable 39 | 40 | # run lint 41 | - name: Run tests 42 | run: yarn lint 43 | 44 | # playwright recommends if you cache the binaries to keep it tied to the version of playwright you are using. 45 | # https://playwright.dev/docs/ci#caching-browsers 46 | - name: Get current Playwright version 47 | id: playwright-version 48 | run: | 49 | echo version=$(npm info @playwright/test version) >> $GITHUB_OUTPUT 50 | 51 | - name: Cache Playwright binaries 52 | uses: actions/cache@v4 53 | id: playwright-cache 54 | with: 55 | path: | 56 | **/node_modules/playwright 57 | ~/.cache/ms-playwright 58 | ~/Library/Caches/ms-playwright 59 | %USERPROFILE%\AppData\Local\ms-playwright 60 | key: ${{ runner.os }}-${{ runner.arch }}-cache-playwright-${{ steps.playwright-version.outputs.version }} 61 | 62 | - name: Playwright info 63 | run: | 64 | echo "OS: ${{ matrix.os }}" 65 | echo "Playwright version: ${{ steps.playwright-version.outputs.version }}" 66 | echo "Playwright install dir: ~/.cache/ms-playwright" 67 | echo "Cache key: ${{ runner.os }}-${{ runner.arch }}-cache-playwright-${{ steps.playwright-version.outputs.version }}" 68 | echo "Cache hit: ${{ steps.playwright-cache.outputs.cache-hit == 'true' }}" 69 | 70 | - name: Install Playwright 71 | if: steps.playwright-cache.outputs.cache-hit != 'true' 72 | run: npx playwright install --with-deps 73 | 74 | # run tests 75 | - name: Run tests 76 | run: TEST_PROJECT_LIST="${{matrix.TEST_PROJECT_LIST}}" yarn test 77 | 78 | - uses: actions/upload-artifact@v4 79 | if: failure() 80 | with: 81 | name: "playwright-report-${{ matrix.os }}" 82 | path: | 83 | test-results/ 84 | playwright-report/ 85 | tests/**/__screenshots__/github_* 86 | retention-days: 5 87 | compression-level: 9 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | *.iml 7 | *.iws 8 | /out 9 | /build 10 | /.idea 11 | .DS_Store 12 | /node_modules 13 | /dist 14 | *.log 15 | package-lock.json 16 | /test-results/ 17 | /playwright-report/ 18 | /playwright/.cache/ 19 | .env.local 20 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.ts": [ 3 | "npm run fix" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /out 2 | /build 3 | /.idea 4 | /node_modules 5 | /dist 6 | /test-results/ 7 | /playwright-report/ 8 | /playwright/ 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![official JetBrains project](http://jb.gg/badges/official-plastic.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) 2 | [![NPM version](https://img.shields.io/npm/v/kotlin-playground.svg)](https://www.npmjs.com/package/kotlin-playground) 3 | 4 | # Kotlin Playground 5 | 6 | Component that creates Kotlin-aware editors capable of running code from HTML block elements. 7 | 8 | [Examples](https://jetbrains.github.io/kotlin-playground/examples/) 9 | 10 | ## Installation 11 | 12 | ### Use our CDN 13 | 14 | Insert a ` 18 | ``` 19 | 20 | Or, if you need to separate process of loading/conversion, omit the `data-selector` attribute and use a second ` 24 | 25 | 30 | ``` 31 | 32 | You can also overwrite the server where the code will be sent to be compiled and analyzed (for example if you host a server instance that includes your own Kotlin libraries). For that you can set the `data-server` attribute. 33 | 34 | And you can also set a default Kotlin version for code snippets to run on. Bear in mind that the [version set per editor](#customizing-editors) will take precedence though: 35 | 36 | ```html 37 | 42 | ``` 43 | 44 | Fork & clone [the old server repository](https://github.com/JetBrains/kotlin-web-demo) or [the new server](https://github.com/AlexanderPrendota/kotlin-compiler-server). 45 | 46 | ### Host your own instance 47 | 48 | Install Kotlin-playground as dependency via NPM. 49 | 50 | ```bash 51 | npm install kotlin-playground -S 52 | ``` 53 | 54 | And then just use it in your code. 55 | 56 | ```js 57 | // ES5 58 | var playground = require('kotlin-playground'); 59 | 60 | document.addEventListener('DOMContentLoaded', function() { 61 | playground('code'); // attach to all elements 62 | }); 63 | 64 | 65 | // ES6 66 | import playground from 'kotlin-playground'; 67 | 68 | document.addEventListener('DOMContentLoaded', () => { 69 | playground('code'); // attach to all elements 70 | }); 71 | ``` 72 | 73 | ### Use from plugins 74 | 75 | 1) [Kotlin Playground WordPress plugin](https://github.com/Kotlin/kotlin-playground-wp-plugin) — [WordPress](https://wordpress.com/) plugin which allows to embed interactive Kotlin playground to any post. 76 | 2) [Kotlin Playground Coursera plugin](https://github.com/AlexanderPrendota/kotlin-playground-coursera-plugin) — Allows embedding interactive Kotlin playground for [coursera](https://www.coursera.org/) lessons. 77 | 3) [Kotlin Playground Orchid plugin](https://orchid.netlify.com/plugins/OrchidSyntaxHighlighter#kotlin-playground) — Allows embedding interactive Kotlin playground in [Orchid](https://orchid.netlify.com/) documentation sites. 78 | 79 | ### Options 80 | 81 | Kotlin Playground supports several events, and also Kotlin version or server URL overwriting passing an additional `options` parameter on initialisation. 82 | 83 | For example: 84 | ```js 85 | function onChange(code) { 86 | console.log("Editor code was changed:\n" + code); 87 | } 88 | 89 | function onTestPassed() { 90 | console.log("Tests passed!"); 91 | } 92 | 93 | const options = { 94 | server: 'https://my-kotlin-playground-server', 95 | version: '1.3.50', 96 | onChange: onChange, 97 | onTestPassed: onTestPassed, 98 | callback: callback(targetNode, mountNode) 99 | }; 100 | 101 | playground('.selector', options) 102 | 103 | ``` 104 | 105 | **Events description:** 106 | 107 | - `onChange(code)` — Fires every time the content of the editor is changed. Debounce time: 0.5s. 108 | _code_ — current playground code. 109 | 110 | - `onTestPassed` — Is called after all tests passed. Use for target platform `junit`. 111 | 112 | - `onTestFailed` — Is called after all tests failed. Use for target platform `junit`. 113 | 114 | - `onCloseConsole` — Is called after the console's closed. 115 | 116 | - `onOpenConsole` — Is called after the console's opened. 117 | 118 | - `getJsCode(code)` — Is called after compilation Kotlin to JS. Use for target platform `js`. 119 | _code_ — converted JS code from Kotlin. 120 | 121 | - `callback(targetNode, mountNode)` — Is called after playground's united. 122 | _targetNode_ — node with plain text before component initialization. 123 | _mountNode_ — new node with runnable editor. 124 | 125 | - `getInstance(instance)` - Getting playground state API. 126 | 127 | ```js 128 | instance.state // playground attributes, dependencies and etc. 129 | instance.nodes // playground NodeElement. 130 | instance.codemirror // editor specification. 131 | instance.execute() // function for executing code snippet. 132 | instance.getCode() // function for getting code from snippet. 133 | ``` 134 | 135 | ## Customizing editors 136 | 137 | 138 | Use the following attributes on elements that are converted to editors to adjust their behavior. 139 | 140 | - `data-version`: Target Kotlin [compiler version](https://api.kotlinlang.org/versions): 141 | 142 | ```html 143 | 144 | /* 145 | Your code here 146 | */ 147 | 148 | ``` 149 | - `args`: Command line arguments. 150 | 151 | ```html 152 | 153 | /* 154 | Your code here 155 | */ 156 | 157 | ``` 158 | 159 | - `data-target-platform`: target platform: `junit`, `canvas`, `js` or `java` (default). 160 | 161 | ```html 162 | 163 | /* 164 | Your code here 165 | */ 166 | 167 | ``` 168 | - `data-highlight-only`: Read-only mode, with only highlighting. `data-highlight-only="nocursor"` - no focus on editor. 169 | 170 | ```html 171 | 172 | /* 173 | Your code here 174 | */ 175 | 176 | ``` 177 | 178 | Or, you can make only a part of code read-only by placing it between `//sampleStart` and `//sampleEnd` markers. 179 | If you don't need this just use attribute `none-markers`. 180 | For adding hidden files: put files between ` 196 | 197 | ``` 198 | Also if you want to hide code snippet just set the attribute `folded-button` to `false` value. 199 | 200 | - `data-js-libs`: By default component loads jQuery and makes it available to the code running in the editor. If you need any additional JS libraries, specify them as comma-separated list in this attribute. 201 | 202 | ```html 203 | 204 | /* 205 | Your code here 206 | */ 207 | 208 | ``` 209 | 210 | - `auto-indent="true|false"`: Whether to use the context-sensitive indentation. Defaults to `false`. 211 | 212 | - `theme="idea|darcula|default"`: Editor IntelliJ IDEA themes. 213 | 214 | - `mode="kotlin|js|java|groovy|xml|c|shell|swift|obj-c"`: Different languages styles. Runnable snippets only with `kotlin`. Default to `kotlin`. 215 | 216 | - `data-min-compiler-version="1.0.7"`: Minimum target Kotlin [compiler version](https://api.kotlinlang.org/versions) 217 | 218 | - `data-autocomplete="true|false"`: Get completion on every key press. If `false` => Press ctrl-space to activate autocompletion. Defaults to `false`. 219 | 220 | - `highlight-on-fly="true|false"`: Errors and warnings check for each change in the editor. Defaults to `false`. 221 | 222 | - `indent="4"`: How many spaces a block should be indented. Defaults to `4`. 223 | 224 | - `lines="true|false"`: Whether to show line numbers to the left of the editor. Defaults to `false`. 225 | 226 | - `from="5" to="10"`: Create a part of code. Example `from` line 5 `to` line 10. 227 | 228 | - `data-output-height="200"`: Set the iframe height in `px` in output. Use for target platform `canvas`. 229 | 230 | - `match-brackets="true|false"`: Determines whether brackets are matched whenever the cursor is moved next to a bracket. Defaults to `false` 231 | 232 | - `data-crosslink="enabled|disabled"`: Show link for open in playground. Defaults to `undefined` – only supported in playground. 233 | 234 | - `data-shorter-height="100"`: show expander if height more than value of attribute 235 | 236 | - `data-scrollbar-style`: Chooses a [scrollbar implementation](https://codemirror.net/doc/manual.html#config). Defaults to `overlay`. 237 | 238 | ## Supported keyboard shortcuts 239 | 240 | - Ctrl+Space — code completion 241 | - Ctrl+F9/Cmd+R — execute snippet 242 | - Ctrl+/ — comment code 243 | - Ctrl+Alt+L/Cmd+Alt+L — format code 244 | - Shift+Tab — decrease indent 245 | - Ctrl+Alt+H/Cmd+Alt+H — highlight code 246 | - Ctrl+Alt+Enter/Cmd+Alt+Enter — show import suggestions 247 | 248 | 249 | ## Develop and contribute 250 | 251 | 1. Fork & clone [our repository](https://github.com/JetBrains/kotlin-playground). 252 | 2. Install required dependencies `yarn install`. 253 | 3. `yarn start` to start local development server at http://localhost:9000. 254 | 4. `yarn test` to run tests. 255 | `TEST_HEADLESS_MODE=true` to run tests in headless mode. 256 | 5. `yarn run build` to create production bundles. 257 | -------------------------------------------------------------------------------- /examples.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kotlin Playground examples 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | # Kotlin Playground demo 19 | 20 | ## Automatic initialization 21 | 22 | Insert a ` 25 | ``` 26 | 27 | For instance following block of Kotlin code: 28 | 29 | ```txt 30 | class Contact(val id: Int, var email: String) 31 | 32 | fun main(args: Array) { 33 | val contact = Contact(1, "mary@gmail.com") 34 | println(contact.id) 35 | } 36 | ``` 37 | 38 | Turns into: 39 | 40 |
41 | 42 | ```kotlin 43 | class Contact(val id: Int, var email: String) 44 | 45 | fun main(args: Array) { 46 | val contact = Contact(1, "mary@gmail.com") 47 | println(contact.id) 48 | } 49 | ``` 50 | 51 |
52 | 53 | You can also change the playground theme or disable run button using `theme` and `data-highlight-only` attributes. 54 | 55 | ```html 56 |
57 | ``` 58 |
59 | 60 | ```kotlin 61 | fun main(args: Array) { 62 | println("Hello World!") 63 | } 64 | ``` 65 | 66 |
67 | 68 | Or theme `kotlin-dark` 69 | 70 |
71 | 72 | ```kotlin 73 | fun main(args: Array) { 74 | println("Hello World!") 75 | } 76 | ``` 77 | 78 |
79 | 80 | Set another target platform with attribute `data-target-platform`. 81 | 82 | ```html 83 |
84 | ``` 85 |
86 | 87 | ```kotlin 88 | fun sum(a: Int, b: Int): Int { 89 | return a + b 90 | } 91 | 92 | fun main(args: Array) { 93 | print(sum(-1, 8)) 94 | } 95 | ``` 96 | 97 |
98 | 99 | You can use JS IR compiler also. 100 | 101 | ```html 102 |
103 | ``` 104 |
105 | 106 | ```kotlin 107 | fun mul(a: Int, b: Int): Int { 108 | return a * b 109 | } 110 | 111 | fun main(args: Array) { 112 | print(mul(-2, 4)) 113 | } 114 | ``` 115 | 116 |
117 | 118 | You can use Wasm compiler. 119 | 120 | ```html 121 |
122 | ``` 123 |
124 | 125 | ```kotlin 126 | fun mul(a: Int, b: Int): Int { 127 | return a * b 128 | } 129 | 130 | fun main(args: Array) { 131 | print(mul(-2, 4)) 132 | println(" + 7 =") 133 | print(mul(-2, 4) + 7) 134 | } 135 | ``` 136 | 137 |
138 | 139 | You can use Compose Wasm. 140 | 141 | ```html 142 |
143 | ``` 144 |
145 | 146 | ```kotlin 147 | import androidx.compose.ui.ExperimentalComposeUiApi 148 | import androidx.compose.ui.window.CanvasBasedWindow 149 | import androidx.compose.animation.AnimatedVisibility 150 | import androidx.compose.foundation.Image 151 | import androidx.compose.foundation.layout.Column 152 | import androidx.compose.foundation.layout.fillMaxWidth 153 | import androidx.compose.material.Button 154 | import androidx.compose.material.MaterialTheme 155 | import androidx.compose.material.Text 156 | import androidx.compose.runtime.Composable 157 | import androidx.compose.runtime.getValue 158 | import androidx.compose.runtime.mutableStateOf 159 | import androidx.compose.runtime.remember 160 | import androidx.compose.runtime.setValue 161 | import androidx.compose.ui.Alignment 162 | import androidx.compose.ui.Modifier 163 | 164 | //sampleStart 165 | @OptIn(ExperimentalComposeUiApi::class) 166 | fun main() { 167 | CanvasBasedWindow { App() } 168 | } 169 | 170 | @Composable 171 | fun App() { 172 | MaterialTheme { 173 | var greetingText by remember { mutableStateOf("Hello World!") } 174 | var showImage by remember { mutableStateOf(false) } 175 | var counter by remember { mutableStateOf(0) } 176 | Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { 177 | Button(onClick = { 178 | counter++ 179 | greetingText = "Compose: ${Greeting().greet()}" 180 | showImage = !showImage 181 | }) { 182 | Text(greetingText) 183 | } 184 | AnimatedVisibility(showImage) { 185 | Text(counter.toString()) 186 | } 187 | } 188 | } 189 | } 190 | 191 | private val platform = object : Platform { 192 | 193 | override val name: String 194 | get() = "Web with Kotlin/Wasm" 195 | } 196 | 197 | fun getPlatform(): Platform = platform 198 | 199 | class Greeting { 200 | private val platform = getPlatform() 201 | 202 | fun greet(): String { 203 | return "Hello, ${platform.name}!" 204 | } 205 | } 206 | 207 | interface Platform { 208 | val name: String 209 | } 210 | //sampleEnd 211 | 212 | ``` 213 | 214 |
215 | 216 | 217 | You can try Kotlin export to Swift. 218 | 219 | ```html 220 |
221 | ``` 222 |
223 | 224 | ```kotlin 225 | fun mul(a: Int, b: Int): Int { 226 | return a * b 227 | } 228 | 229 | fun main(args: Array) { 230 | print(mul(-2, 4)) 231 | println(" + 7 =") 232 | print(mul(-2, 4) + 7) 233 | } 234 | ``` 235 | 236 |
237 | 238 |
239 | 240 | ```kotlin 241 | fun mul(a: Int, b: Int): Int { 242 | return a * b 243 | } 244 | 245 | fun main(args: Array) { 246 | print(mul(-2, 4)) 247 | println(" + 7 =") 248 | print(mul(-2, 4) + 7) 249 | } 250 | ``` 251 | 252 |
253 | 254 | Use `data-target-platform` attribute with value `junit` for creating examples with tests: 255 | 256 |
257 | 258 | ```kotlin 259 | import org.junit.Test 260 | import org.junit.Assert 261 | 262 | class TestExtensionFunctions() { 263 | @Test fun testIntExtension() { 264 | Assert.assertEquals("Rational number creation error: ", RationalNumber(4, 1), 4.r()) 265 | } 266 | 267 | @Test fun testPairExtension() { 268 | Assert.assertEquals("Rational number creation error: ", RationalNumber(2, 3), Pair(2, 3).r()) 269 | } 270 | } 271 | //sampleStart 272 | /* 273 | Then implement extension functions Int.r() and Pair.r() and make them convert Int and Pair to RationalNumber. 274 | */ 275 | fun Int.r(): RationalNumber = RationalNumber(this, 2) 276 | fun Pair.r(): RationalNumber = RationalNumber(first, second) 277 | 278 | data class RationalNumber(val numerator: Int, val denominator: Int) 279 | //sampleEnd 280 | ``` 281 |
282 | 283 | The test clases in this code snippet are folded away thanks to the `//sampleStart` and `//sampleEnd` comments in the code. 284 | If you want to hide test classes completely just set the attribute `folded-button` to `false` value. 285 | 286 | Also you can mark arbitrary code by putting it between `[mark]your code[/mark]`. 287 | 288 | ```html 289 |
290 | ``` 291 | 292 |
293 | 294 | ```kotlin 295 | import org.junit.Test 296 | import org.junit.Assert 297 | 298 | class TestLambdas() { 299 | @Test fun contains() { 300 | Assert.assertTrue("The result should be true if the collection contains an even number", 301 | containsEven(listOf(1, 2, 3, 126, 555))) 302 | } 303 | 304 | @Test fun notContains() { 305 | Assert.assertFalse("The result should be false if the collection doesn't contain an even number", 306 | containsEven(listOf(43, 33))) 307 | } 308 | } 309 | //sampleStart 310 | /* 311 | Pass a lambda to any function to check if the collection contains an even number. 312 | The function any gets a predicate as an argument and returns true if there is at least one element satisfying the predicate. 313 | */ 314 | fun containsEven(collection: Collection): Boolean = collection.any {[mark]TODO()[/mark]} 315 | //sampleEnd 316 | ``` 317 | 318 |
319 | 320 | Use `data-target-platform` attribute with value `canvas` for working with canvas in Kotlin: 321 | 322 | ```html 323 |
324 | ``` 325 | 326 |
327 | 328 | ```kotlin 329 | package fancylines 330 | 331 | import org.w3c.dom.CanvasRenderingContext2D 332 | import org.w3c.dom.HTMLCanvasElement 333 | import kotlinx.browser.document 334 | import kotlinx.browser.window 335 | import kotlin.random.Random 336 | 337 | 338 | 339 | val canvas = initalizeCanvas() 340 | fun initalizeCanvas(): HTMLCanvasElement { 341 | val canvas = document.createElement("canvas") as HTMLCanvasElement 342 | val context = canvas.getContext("2d") as CanvasRenderingContext2D 343 | context.canvas.width = window.innerWidth.toInt(); 344 | context.canvas.height = window.innerHeight.toInt(); 345 | document.body!!.appendChild(canvas) 346 | return canvas 347 | } 348 | 349 | class FancyLines() { 350 | val context = canvas.getContext("2d") as CanvasRenderingContext2D 351 | val height = canvas.height 352 | val width = canvas.width 353 | var x = width * Random.nextDouble() 354 | var y = height * Random.nextDouble() 355 | var hue = 0; 356 | 357 | fun line() { 358 | context.save(); 359 | 360 | context.beginPath(); 361 | 362 | context.lineWidth = 20.0 * Random.nextDouble(); 363 | context.moveTo(x, y); 364 | 365 | x = width * Random.nextDouble(); 366 | y = height * Random.nextDouble(); 367 | 368 | context.bezierCurveTo(width * Random.nextDouble(), height * Random.nextDouble(), 369 | width * Random.nextDouble(), height * Random.nextDouble(), x, y); 370 | 371 | hue += (Random.nextDouble() * 10).toInt(); 372 | 373 | context.strokeStyle = "hsl($hue, 50%, 50%)"; 374 | 375 | context.shadowColor = "white"; 376 | context.shadowBlur = 10.0; 377 | 378 | context.stroke(); 379 | 380 | context.restore(); 381 | } 382 | 383 | fun blank() { 384 | context.fillStyle = "rgba(255,255,1,0.1)"; 385 | context.fillRect(0.0, 0.0, width.toDouble(), height.toDouble()); 386 | } 387 | 388 | fun run() { 389 | window.setInterval({ line() }, 40); 390 | window.setInterval({ blank() }, 100); 391 | } 392 | } 393 | //sampleStart 394 | fun main(args: Array) { 395 | FancyLines().run() 396 | } 397 | //sampleEnd 398 | ``` 399 | 400 |
401 | 402 | Use `data-js-libs` with a comma-separated list of URLs to specify additional javascript libraries to load. 403 | 404 | ```html 405 |
406 | ``` 407 | 408 |
409 | 410 | ```kotlin 411 | external fun moment(): dynamic 412 | 413 | fun main() { 414 | val startOfDay = moment().startOf("day").fromNow() 415 | println("The start of the day was $startOfDay") 416 | } 417 | ``` 418 | 419 |
420 | 421 | 422 |
423 | 424 | ```kotlin 425 | external fun moment(): dynamic 426 | 427 | fun main() { 428 | val startOfDay = moment().startOf("day").fromNow() 429 | println("The start of the day was $startOfDay") 430 | } 431 | ``` 432 | 433 |
434 | 435 | ## Manual initialization 436 | 437 | If you want to init Kotlin Playground manually - omit `data-selector` attribute and call it when it's needed: 438 | 439 | ```html 440 | 441 | 446 | ``` 447 | Add additional hidden files: 448 | Put your files between ` 552 | 553 |
554 | 555 | 556 | 557 | -------------------------------------------------------------------------------- /examples/examples-highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kotlin-playground", 3 | "version": "1.32.0", 4 | "description": "Self-contained component to embed in websites for running Kotlin code", 5 | "keywords": [ 6 | "kotlin", 7 | "runnable code" 8 | ], 9 | "author": "JetBrains", 10 | "license": "Apache-2.0", 11 | "repository": "JetBrains/kotlin-playground", 12 | "main": "dist/playground.min.js", 13 | "files": [ 14 | "dist", 15 | "!dist/REMOVE_ME.js*" 16 | ], 17 | "devDependencies": { 18 | "@babel/cli": "^7.25.9", 19 | "@babel/core": "^7.25.9", 20 | "@babel/plugin-transform-runtime": "^7.25.9", 21 | "@babel/preset-env": "^7.25.9", 22 | "@babel/runtime-corejs2": "^7.25.9", 23 | "@playwright/test": "^1.48.1", 24 | "@typescript-eslint/eslint-plugin": "^6.21.0", 25 | "@typescript-eslint/parser": "^6.21.0", 26 | "babel-loader": "^9.2.1", 27 | "babel-plugin-async-to-promises": "^1.0.5", 28 | "ci-publish": "^1.3.1", 29 | "codemirror": "^5.65.18", 30 | "css-loader": "^6.8.1", 31 | "debounce": "^1.2.0", 32 | "deepmerge": "^1.5.0", 33 | "dotenv": "^16.4.5", 34 | "es6-promise": "^4.2.6", 35 | "es6-set": "^0.1.5", 36 | "escape-html": "^1.0.3", 37 | "eslint": "^8.57.1", 38 | "eslint-config-prettier": "^9.1.0", 39 | "eslint-plugin-import": "^2.31.0", 40 | "eslint-plugin-prettier": "^5.2.1", 41 | "fast-async": "7", 42 | "fast-deep-equal": "^3.1.3", 43 | "file-loader": "^6.2.0", 44 | "flatten": "^1.0.2", 45 | "github-markdown-css": "^3.0.1", 46 | "html-webpack-plugin": "^5.6.3", 47 | "husky": "^8.0.3", 48 | "is-empty-object": "^1.1.1", 49 | "lint-staged": "^15.2.10", 50 | "lz-string": "^1.4.4", 51 | "markdown-it": "^12.3.2", 52 | "markdown-it-highlightjs": "^3.0.0", 53 | "mime": "^3.0.0", 54 | "monkberry": "4.0.8", 55 | "monkberry-directives": "4.0.8", 56 | "monkberry-events": "4.0.8", 57 | "monkberry-loader": "4.0.9", 58 | "postcss": "^8.4.47", 59 | "postcss-loader": "^7.3.4", 60 | "prettier": "^3.3.3", 61 | "query-string": "^6.5.0", 62 | "sass": "^1.80.4", 63 | "sass-loader": "^13.3.3", 64 | "shelljs": "^0.8.3", 65 | "style-loader": "^3.3.4", 66 | "svg-fill-loader": "^0.0.8", 67 | "svg-url-loader": "^8.0.0", 68 | "ts-loader": "^9.5.1", 69 | "typescript": "^5.6.3", 70 | "url-search-params": "1.1.0", 71 | "webpack": "^5.91.0", 72 | "webpack-cli": "^5.1.4", 73 | "webpack-dev-server": "^4.15.2", 74 | "whatwg-fetch": "^3.6.20" 75 | }, 76 | "scripts": { 77 | "build": "webpack", 78 | "build:all": "npm run build && npm run build:production", 79 | "build:production": "webpack --env production", 80 | "copy-examples": "node utils/copy-examples", 81 | "release:ci": "rm -rf dist && npm run build:all && $NPM_TOKEN=%env.NPM_TOKEN% npm publish", 82 | "start": "webpack-dev-server --port 9002", 83 | "start-with-local-compiler": "webpack-dev-server --port 9002 --env webDemoUrl='//localhost:8080' webDemoResourcesUrl='//localhost:8081'", 84 | "lint": "eslint . --ext .ts", 85 | "fix": "eslint --fix --ext .ts .", 86 | "test": "npm run build:all && npm run test:run", 87 | "test:run": "playwright test", 88 | "test:server": "node tests/utils/server/index.js", 89 | "prepare": "husky install" 90 | }, 91 | "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" 92 | } 93 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'process'; 2 | import { config as dotenv } from 'dotenv'; 3 | import { defineConfig, devices } from '@playwright/test'; 4 | import { isKeyOfObject } from './src/utils/types'; 5 | 6 | dotenv({ path: '.env.local', override: true }); 7 | 8 | const PROJECTS_LIST = { 9 | DEV: ['Desktop Chrome'], 10 | GITHUB_MAC: ['Desktop Safari'], 11 | GITHUB_LINUX: ['Desktop Chrome', 'Desktop Firefox'], 12 | }; 13 | 14 | const mode = (env.TEST_PROJECT_LIST || 'DEV').toUpperCase(); 15 | 16 | if (!(mode && isKeyOfObject(mode, PROJECTS_LIST))) { 17 | const list = Object.keys(PROJECTS_LIST) 18 | .map((s) => `'${s.toLowerCase()}'`) 19 | .join(' or '); 20 | 21 | throw Error(`TEST_PROJECT_LIST should be ${list}`); 22 | } 23 | 24 | const isDevMode = Boolean(mode === 'DEV'); 25 | 26 | export default defineConfig({ 27 | testDir: './tests', 28 | testMatch: /.*\.(e2e|test)\.tsx?$/, 29 | snapshotPathTemplate: `{testDir}/{testFileDir}/__screenshots__/${mode.toLowerCase()}/{projectName}/{testFilePath}-{arg}{ext}`, 30 | 31 | timeout: 30000, 32 | forbidOnly: !isDevMode, 33 | reporter: 'list', 34 | retries: isDevMode ? 0 : 2, 35 | fullyParallel: !isDevMode, 36 | 37 | webServer: { 38 | command: 'npm run test:server', 39 | port: 8000, 40 | reuseExistingServer: isDevMode, 41 | }, 42 | 43 | use: { 44 | testIdAttribute: 'data-test', 45 | headless: ((value) => (value ? value === 'true' : !isDevMode))( 46 | env.TEST_HEADLESS_MODE, 47 | ), 48 | ignoreHTTPSErrors: true, 49 | screenshot: { 50 | fullPage: true, 51 | mode: isDevMode ? 'only-on-failure' : 'on', 52 | }, 53 | trace: isDevMode ? 'on-first-retry' : 'on', 54 | video: isDevMode ? 'on-first-retry' : 'on', 55 | }, 56 | 57 | projects: PROJECTS_LIST[mode].map((project) => ({ 58 | name: project, 59 | use: { ...devices[project] }, 60 | })), 61 | }); 62 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { getConfigFromElement, getCurrentScript } from './utils'; 2 | import { TargetPlatforms } from './utils/platforms'; 3 | 4 | const currentScript = getCurrentScript(); 5 | 6 | export const RUNTIME_CONFIG = { ...getConfigFromElement(currentScript) }; 7 | 8 | /** 9 | * API Paths 10 | * 11 | * @type {{COMPILE: string, COMPLETE: string, VERSIONS: string, JQUERY: string, KOTLIN_JS: string}} 12 | */ 13 | export const API_URLS = { 14 | server: (RUNTIME_CONFIG.server || __WEBDEMO_URL__).replace(/\/$/, ''), 15 | composeServer: 'https://compose-stage.sandbox.intellij.net'.replace( 16 | /\/$/, 17 | '', 18 | ), 19 | 20 | COMPILE(platform, version) { 21 | let url; 22 | 23 | switch (platform) { 24 | case TargetPlatforms.JAVA: 25 | url = `${this.server}/api/${version}/compiler/run`; 26 | break; 27 | case TargetPlatforms.CANVAS: 28 | url = `${this.server}/api/${version}/compiler/translate`; 29 | break; 30 | case TargetPlatforms.JS: 31 | url = `${this.server}/api/${version}/compiler/translate`; 32 | break; 33 | case TargetPlatforms.JS_IR: 34 | url = `${this.server}/api/${version}/compiler/translate?ir=true`; 35 | break; 36 | case TargetPlatforms.WASM: 37 | url = `${this.server}/api/${version}/compiler/translate?ir=true&compiler=wasm`; 38 | break; 39 | case TargetPlatforms.COMPOSE_WASM: 40 | url = `${this.composeServer}/api/compiler/translate?compiler=${TargetPlatforms.COMPOSE_WASM.id}`; 41 | break; 42 | case TargetPlatforms.JUNIT: 43 | url = `${this.server}/api/${version}/compiler/test`; 44 | break; 45 | case TargetPlatforms.SWIFT_EXPORT: 46 | url = `${this.server}/api/${version}/${TargetPlatforms.SWIFT_EXPORT.id}/compiler/translate?compiler=swift-export`; 47 | break; 48 | default: 49 | console.warn(`Unknown ${platform.id} , used by default JVM`); 50 | url = `${this.server}/api/${version}/compiler/run`; 51 | break; 52 | } 53 | 54 | return url; 55 | }, 56 | 57 | HIGHLIGHT(version) { 58 | return `${this.server}/api/${version}/compiler/highlight`; 59 | }, 60 | COMPLETE(version) { 61 | return `${this.server}/api/${version}/compiler/complete`; 62 | }, 63 | get VERSIONS() { 64 | return `${this.server}/versions`; 65 | }, 66 | RESOURCE_VERSIONS() { 67 | return `${this.composeServer}/api/resource/compose-wasm-versions`; 68 | }, 69 | SKIKO_MJS(version) { 70 | return `${this.composeServer}/api/resource/skiko-${version}.mjs`; 71 | }, 72 | SKIKO_WASM(version) { 73 | return `${this.composeServer}/api/resource/skiko-${version}.wasm`; 74 | }, 75 | STDLIB_MJS(hash) { 76 | return `${this.composeServer}/api/resource/stdlib-${hash}.mjs`; 77 | }, 78 | STDLIB_WASM(hash) { 79 | return `${this.composeServer}/api/resource/stdlib-${hash}.wasm`; 80 | }, 81 | get JQUERY() { 82 | return `https://cdn.jsdelivr.net/npm/jquery@1/dist/jquery.min.js`; 83 | }, 84 | get KOTLIN_JS() { 85 | return `https://cdn.jsdelivr.net/npm/kotlin@`; 86 | }, 87 | }; 88 | 89 | /** 90 | * @typedef {Object} KotlinRunCodeConfig 91 | */ 92 | export default { 93 | selector: 'code', 94 | 95 | /** 96 | * Will be calculated according to user defined `data-min-compiler-version` 97 | * attribute and WebDemo API response 98 | */ 99 | compilerVersion: undefined, 100 | }; 101 | -------------------------------------------------------------------------------- /src/discourse-preview-panel-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This will handle user input changes and render editor in the preview panel 3 | */ 4 | import debounce from 'debounce'; 5 | 6 | import { arrayFrom } from './utils'; 7 | 8 | export const Selectors = { 9 | PREVIEW_PANEL: '.d-editor-preview', 10 | PREVIEW_TEXTAREA: '.d-editor-input.ember-text-area', 11 | KOTLIN_CODE_BLOCK: '.lang-run-kotlin', 12 | }; 13 | 14 | const DEBOUNCE_TIME = 300; 15 | 16 | export default function () { 17 | const kotlinRunCodeGlobalObject = window[__LIBRARY_NAME__]; 18 | const textarea = document.querySelector(Selectors.PREVIEW_TEXTAREA); 19 | const previewPanel = document.querySelector(Selectors.PREVIEW_PANEL); 20 | 21 | if (!textarea || !previewPanel) { 22 | return; 23 | } 24 | 25 | textarea.addEventListener('keydown', debounce(() => { 26 | const previewCodeBlocks = previewPanel.querySelectorAll(Selectors.KOTLIN_CODE_BLOCK); 27 | 28 | arrayFrom(previewCodeBlocks).forEach(node => { 29 | const previousKotlinRunCodeInstance = node[__LIBRARY_NAME__]; 30 | if (previousKotlinRunCodeInstance) { 31 | previousKotlinRunCodeInstance.destroy(); 32 | } 33 | kotlinRunCodeGlobalObject(node); 34 | }); 35 | }, DEBOUNCE_TIME)); 36 | } 37 | -------------------------------------------------------------------------------- /src/executable-code/exception.js: -------------------------------------------------------------------------------- 1 | import Exception from './exception.monk'; 2 | 3 | export default class extends Exception { 4 | constructor() { 5 | super(); 6 | this.state = {onExceptionClick: null}; 7 | } 8 | 9 | update(state) { 10 | Object.assign(this.state, state); 11 | super.update(state); 12 | } 13 | 14 | onStackTraceClick(fileName, line) { 15 | this.state.onExceptionClick(fileName, line); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/executable-code/exception.monk: -------------------------------------------------------------------------------- 1 | Exception in thread "main" {{ fullName }}{% if message %}: {{ message }} {% endif %} 2 |
3 | {% for stacktraceElement of stackTrace %} 4 | 5 | at {{ stacktraceElement.className }}.{{ stacktraceElement.methodName }} 6 | ( 9 | {{ stacktraceElement.fileName }}:{{ stacktraceElement.lineNumber }} 10 | ) 11 | 12 |
13 | {% endfor %} 14 | 15 | {% for cause of causes %} 16 | Caused by: {{ cause.fullName }}{% if cause.message %}: {{ cause.message }} {% endif %}
17 | {% for stacktraceElement of cause.stackTrace %} 18 | at {{ stacktraceElement.className }} 19 | .{{ stacktraceElement.methodName }}({{ stacktraceElement.fileName }}:{{ stacktraceElement.lineNumber }}) 20 |
21 | {% endfor %} 22 | {% endfor %} 23 | -------------------------------------------------------------------------------- /src/executable-code/executable-fragment.monk: -------------------------------------------------------------------------------- 1 | {% import Exception from './exception' %} 2 | 3 |
4 |
5 | {% if (!highlightOnly) %} 6 |
8 | {% endif %} 9 |
10 | {% if (isShouldBeFolded) %} 11 |
15 |
16 | {% endif %} 17 | 18 |
19 |
20 | {% if (openConsole) %} 21 |
22 | {% endif %} 23 | {% if (waitingForOutput) %} 24 |
25 |
26 |
27 | {% else %} 28 | {% if (output && output != "") || exception %} 29 |
30 |
31 | {% unsafe output %} 32 | 33 | {% if exception %} 34 | 39 | {% endif %} 40 |
41 |
42 | {% endif %} 43 | {% endif %} 44 |
45 |
46 | 47 | {% if (!highlightOnly) %} 48 | 57 | {% endif %} 58 | {% if (shorterHeight) %}{% endif %} 59 |
60 | -------------------------------------------------------------------------------- /src/executable-code/index.js: -------------------------------------------------------------------------------- 1 | import 'codemirror'; 2 | import 'codemirror/lib/codemirror'; 3 | import 'codemirror/addon/hint/show-hint' 4 | import 'codemirror/addon/hint/anyword-hint' 5 | import 'codemirror/addon/scroll/simplescrollbars' 6 | import 'codemirror/mode/smalltalk/smalltalk' 7 | import 'codemirror/addon/runmode/colorize'; 8 | import 'codemirror/mode/clike/clike'; 9 | import 'codemirror/mode/groovy/groovy'; 10 | import 'codemirror/mode/xml/xml'; 11 | import 'codemirror/addon/edit/matchbrackets' 12 | import 'codemirror/addon/edit/closebrackets' 13 | import 'codemirror/addon/comment/comment' 14 | import 'codemirror/addon/comment/continuecomment' 15 | import 'codemirror/mode/javascript/javascript'; 16 | import 'codemirror/mode/shell/shell'; 17 | import 'codemirror/mode/swift/swift'; 18 | import merge from 'deepmerge'; 19 | import Set from 'es6-set/polyfill'; 20 | import defaultConfig, {API_URLS} from '../config'; 21 | import {arrayFrom, getConfigFromElement, insertAfter, READ_ONLY_TAG, replaceWhiteSpaces, THEMES} from '../utils'; 22 | import WebDemoApi from "../webdemo-api"; 23 | import ExecutableFragment from './executable-fragment'; 24 | import { generateCrosslink } from '../lib/crosslink'; 25 | import '../styles.scss'; 26 | import {getTargetById, isJsRelated, isWasmRelated, TargetPlatforms} from "../utils/platforms"; 27 | 28 | const INITED_ATTRIBUTE_NAME = 'data-kotlin-playground-initialized'; 29 | const DEFAULT_INDENT = 4; 30 | 31 | const ATTRIBUTES = { 32 | SHORTER_HEIGHT: 'data-shorter-height', 33 | HIDDEN_DEPENDENCY: 'hidden-dependency', 34 | INDENT: 'indent', 35 | HIGHLIGHT_ONLY: 'data-highlight-only', 36 | STYLE: 'style', 37 | FROM: 'from', 38 | TO: 'to', 39 | NONE_MARKERS: 'none-markers', 40 | THEME: 'theme', 41 | MODE: 'mode', 42 | MATCH_BRACKETS: 'match-brackets', 43 | OUTPUT_HEIGHT: 'data-output-height', 44 | COMPLETE: 'data-autocomplete', 45 | ON_FLY_HIGHLIGHT: 'highlight-on-fly', 46 | PLATFORM: 'data-target-platform', 47 | JS_LIBS: 'data-js-libs', 48 | FOLDED_BUTTON: 'folded-button', 49 | ARGUMENTS: 'args', 50 | LINES: 'lines', 51 | AUTO_INDENT: 'auto-indent', 52 | TRACK_RUN_ID: 'data-track-run-id', 53 | CROSSLINK: 'data-crosslink', 54 | SCROLLBAR_STYLE: 'data-scrollbar-style' 55 | }; 56 | 57 | const MODES = { 58 | JAVA: "text/x-java", 59 | KOTLIN: "text/x-kotlin", 60 | JS: "text/javascript", 61 | GROOVY: "text/x-groovy", 62 | XML: "text/html", 63 | C: "text/x-c", 64 | OBJ_C: "text/x-objectivec", 65 | SWIFT: "text/x-swift", 66 | SHELL: "text/x-sh" 67 | }; 68 | 69 | export default class ExecutableCode { 70 | /** 71 | * @param {string|HTMLElement} target 72 | * @param {{compilerVersion: *}} [config] 73 | * @param {Object} eventFunctions 74 | */ 75 | constructor(target, config = {}, eventFunctions) { 76 | const targetNode = typeof target === 'string' ? document.querySelector(target) : target; 77 | let highlightOnly = config.highlightOnly ? true 78 | : targetNode.getAttribute(ATTRIBUTES.HIGHLIGHT_ONLY) === READ_ONLY_TAG 79 | ? targetNode.getAttribute(ATTRIBUTES.HIGHLIGHT_ONLY) 80 | : targetNode.hasAttribute(ATTRIBUTES.HIGHLIGHT_ONLY); 81 | const noneMarkers = targetNode.hasAttribute(ATTRIBUTES.NONE_MARKERS); 82 | const indent = targetNode.hasAttribute(ATTRIBUTES.INDENT) ? parseInt(targetNode.getAttribute(ATTRIBUTES.INDENT)) : DEFAULT_INDENT; 83 | const from = targetNode.hasAttribute(ATTRIBUTES.FROM) ? parseInt(targetNode.getAttribute(ATTRIBUTES.FROM)) : null; 84 | const to = targetNode.hasAttribute(ATTRIBUTES.TO) ? parseInt(targetNode.getAttribute(ATTRIBUTES.TO)) : null; 85 | const editorTheme = this.getTheme(targetNode); 86 | const args = targetNode.hasAttribute(ATTRIBUTES.ARGUMENTS) ? targetNode.getAttribute(ATTRIBUTES.ARGUMENTS) : ""; 87 | const hiddenDependencies = this.getHiddenDependencies(targetNode); 88 | const outputHeight = targetNode.getAttribute(ATTRIBUTES.OUTPUT_HEIGHT) || null; 89 | const targetPlatform = getTargetById(targetNode.getAttribute(ATTRIBUTES.PLATFORM)) || TargetPlatforms.JAVA; 90 | const targetNodeStyle = targetNode.getAttribute(ATTRIBUTES.STYLE); 91 | const jsLibs = this.getJsLibraries(targetNode, targetPlatform); 92 | const isFoldedButton = targetNode.getAttribute(ATTRIBUTES.FOLDED_BUTTON) !== "false"; 93 | const lines = targetNode.getAttribute(ATTRIBUTES.LINES) === "true"; 94 | const onFlyHighLight = targetNode.getAttribute(ATTRIBUTES.ON_FLY_HIGHLIGHT) === "true"; 95 | const autoComplete = targetNode.getAttribute(ATTRIBUTES.COMPLETE) === "true"; 96 | const matchBrackets = targetNode.getAttribute(ATTRIBUTES.MATCH_BRACKETS) === "true"; 97 | const autoIndent = targetNode.getAttribute(ATTRIBUTES.AUTO_INDENT) === "true"; 98 | const dataTrackRunId = targetNode.getAttribute(ATTRIBUTES.TRACK_RUN_ID); 99 | const dataShorterHeight = targetNode.getAttribute(ATTRIBUTES.SHORTER_HEIGHT); 100 | const dataScrollbarStyle = targetNode.getAttribute(ATTRIBUTES.SCROLLBAR_STYLE); 101 | const mode = this.getMode(targetNode); 102 | const code = replaceWhiteSpaces(targetNode.textContent); 103 | const cfg = merge(defaultConfig, config); 104 | 105 | // no run code in none kotlin mode 106 | if (mode !== MODES.KOTLIN && highlightOnly !== READ_ONLY_TAG) { 107 | highlightOnly = true; 108 | } 109 | 110 | let crosslink = null; 111 | 112 | const crosslinkValue = targetNode.getAttribute(ATTRIBUTES.CROSSLINK); 113 | 114 | const isCrosslinkDisabled = ( 115 | crosslinkValue !== 'enabled' && ( 116 | crosslinkValue === 'disabled' || // disabled by developer 117 | highlightOnly || // highlighted only not worked in... 118 | ( // Unsupported external deps 119 | (jsLibs && !!jsLibs.size) || 120 | (hiddenDependencies && hiddenDependencies.length > 0) 121 | ) 122 | ) 123 | ); 124 | 125 | if (!isCrosslinkDisabled) crosslink = generateCrosslink(code, { 126 | code: code, 127 | targetPlatform: targetPlatform.id, 128 | // hiddenDependencies, // multi-file support needs 129 | compilerVersion: cfg.compilerVersion, 130 | }); 131 | 132 | let shorterHeight = parseInt(dataShorterHeight, 10) || 0; 133 | 134 | targetNode.style.display = 'none'; 135 | targetNode.setAttribute(INITED_ATTRIBUTE_NAME, 'true'); 136 | const mountNode = document.createElement('div'); 137 | insertAfter(mountNode, targetNode); 138 | 139 | const view = ExecutableFragment.render(mountNode, {eventFunctions}); 140 | view.update(Object.assign({ 141 | code: code, 142 | lines: lines, 143 | theme: editorTheme, 144 | indent: indent, 145 | args: args, 146 | mode: mode, 147 | crosslink, 148 | matchBrackets: matchBrackets, 149 | from: from, 150 | to: to, 151 | autoComplete: autoComplete, 152 | hiddenDependencies: hiddenDependencies, 153 | compilerVersion: cfg.compilerVersion, 154 | noneMarkers: noneMarkers, 155 | onFlyHighLight: onFlyHighLight, 156 | autoIndent: autoIndent, 157 | highlightOnly: highlightOnly, 158 | targetPlatform: targetPlatform, 159 | jsLibs: jsLibs, 160 | isFoldedButton: isFoldedButton, 161 | dataTrackRunId, 162 | shorterHeight, 163 | outputHeight, 164 | scrollbarStyle: dataScrollbarStyle 165 | }, eventFunctions)); 166 | 167 | this.config = cfg; 168 | this.node = mountNode; 169 | this.targetNode = targetNode; 170 | this.targetNodeStyle = targetNodeStyle; 171 | this.view = view; 172 | 173 | targetNode.KotlinPlayground = this; 174 | if (eventFunctions && eventFunctions.callback) eventFunctions.callback(targetNode, mountNode); 175 | } 176 | 177 | /** 178 | * Get all nodes values by {ATTRIBUTES.HIDDEN_DEPENDENCY} selector. 179 | * Node should be `textarea`. 180 | * @param targetNode - {NodeElement} 181 | * @returns {Array} - list of node's text content 182 | */ 183 | getHiddenDependencies(targetNode) { 184 | return arrayFrom(targetNode.getElementsByClassName(ATTRIBUTES.HIDDEN_DEPENDENCY)) 185 | .reduce((acc, node) => { 186 | node.parentNode.removeChild(node); 187 | return [...acc, replaceWhiteSpaces(node.textContent)]; 188 | }, []) 189 | } 190 | 191 | /** 192 | * Add additional JS-library. 193 | * Setting JQuery as default library. 194 | * @param targetNode - {NodeElement} 195 | * @param platform - {TargetPlatform} 196 | * @returns {Set} - set of additional libraries 197 | */ 198 | getJsLibraries(targetNode, platform) { 199 | if (isWasmRelated(platform) || platform === TargetPlatforms.SWIFT_EXPORT) { 200 | return new Set() 201 | } 202 | 203 | if (isJsRelated(platform)) { 204 | const jsLibs = targetNode.getAttribute(ATTRIBUTES.JS_LIBS); 205 | let additionalLibs = new Set(API_URLS.JQUERY.split()); 206 | if (jsLibs) { 207 | let checkUrl = new RegExp("https?://.+$"); 208 | jsLibs 209 | .replace(" ", "") 210 | .split(",") 211 | .filter(lib => checkUrl.test(lib)) 212 | .forEach(lib => additionalLibs.add(lib)); 213 | } 214 | return additionalLibs; 215 | } 216 | } 217 | 218 | getTheme(targetNode) { 219 | const theme = targetNode.getAttribute(ATTRIBUTES.THEME); 220 | 221 | if (theme && theme !== THEMES.DARCULA && theme !== THEMES.IDEA) { 222 | console.warn(`Custom theme '${theme}' requires custom css by developer, you might use default values for reduce size – ${THEMES.DARCULA} or ${THEMES.IDEA}.`); 223 | } 224 | 225 | return theme || THEMES.DEFAULT; 226 | } 227 | 228 | getMode(targetNode) { 229 | const mode = targetNode.getAttribute(ATTRIBUTES.MODE); 230 | switch (mode) { 231 | case "java": 232 | return MODES.JAVA; 233 | case "c": 234 | return MODES.C; 235 | case "js": 236 | return MODES.JS; 237 | case "groovy": 238 | return MODES.GROOVY; 239 | case "xml": 240 | return MODES.XML; 241 | case "shell": 242 | return MODES.SHELL; 243 | case "obj-c": 244 | return MODES.OBJ_C; 245 | case "swift": 246 | return MODES.SWIFT; 247 | default: 248 | return MODES.KOTLIN; 249 | } 250 | } 251 | 252 | destroy() { 253 | this.config = null; 254 | this.node = null; 255 | this.view.destroy(); 256 | const targetNode = this.targetNode; 257 | 258 | if (this.targetNodeStyle !== null) { 259 | targetNode.style = this.targetNodeStyle; 260 | } else { 261 | targetNode.style = ''; 262 | } 263 | 264 | targetNode.removeAttribute(INITED_ATTRIBUTE_NAME); 265 | delete targetNode.KotlinPlayground; 266 | } 267 | 268 | isInited() { 269 | const node = this.targetNode; 270 | const attr = node && node.getAttribute(INITED_ATTRIBUTE_NAME); 271 | return attr && attr === 'true'; 272 | } 273 | 274 | /** 275 | * @param {string|Node|NodeList} target 276 | * @param {Object} options 277 | * @return {Promise>} 278 | */ 279 | static create(target, options) { 280 | let targetNodes; 281 | 282 | if (typeof target === 'string') { 283 | targetNodes = arrayFrom(document.querySelectorAll(target)); 284 | } else if (target instanceof Node) { 285 | targetNodes = [target]; 286 | } else if (target instanceof NodeList === false) { 287 | throw new Error(`'target' type should be string|Node|NodeList, ${typeof target} given`); 288 | } 289 | 290 | // Return empty array if there is no nodes attach to 291 | if (targetNodes.length === 0) { 292 | return Promise.resolve([]); 293 | } 294 | 295 | return WebDemoApi.getCompilerVersions().then((versions) => { 296 | const instances = []; 297 | 298 | targetNodes.forEach((node) => { 299 | const config = getConfigFromElement(node, true); 300 | const minCompilerVersion = config.minCompilerVersion; 301 | let latestStableVersion = null; 302 | let compilerVersion = null; 303 | 304 | // Skip empty and already initialized nodes 305 | if ( 306 | node.textContent.trim() === '' || 307 | node.getAttribute(INITED_ATTRIBUTE_NAME) === 'true' 308 | ) { 309 | return; 310 | } 311 | 312 | if (versions) { 313 | versions.sort(function({ version: version1 }, { version: version2 }) { 314 | if (version1 < version2) return -1; 315 | if (version1 > version2) return 1; 316 | return 0; 317 | }); 318 | 319 | let listOfVersions = versions.map(version => version.version); 320 | 321 | if (listOfVersions.includes(config.version)) { 322 | compilerVersion = config.version; 323 | } else if (listOfVersions.includes(options.version)) { 324 | compilerVersion = options.version; 325 | } else { 326 | versions.forEach((compilerConfig) => { 327 | if (compilerConfig.latestStable) { 328 | latestStableVersion = compilerConfig.version; 329 | } 330 | }); 331 | compilerVersion = latestStableVersion; 332 | } 333 | 334 | if (minCompilerVersion) { 335 | compilerVersion = minCompilerVersion > latestStableVersion 336 | ? versions[versions.length - 1].version 337 | : latestStableVersion; 338 | } 339 | instances.push(new ExecutableCode(node, {compilerVersion}, options)); 340 | } else { 341 | console.error('Cann\'t get kotlin version from server'); 342 | instances.push(new ExecutableCode(node, {highlightOnly: true})); 343 | } 344 | }); 345 | 346 | return instances; 347 | }); 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/img/attention.svg: -------------------------------------------------------------------------------- 1 | 2 | icons_all_sprite 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/button_close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/button_close_darcula.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/button_open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/button_open_darcula.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/img/close-console-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | icons_all_sprite 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/img/fail.svg: -------------------------------------------------------------------------------- 1 | 2 | icons_all_sprite 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/ok.svg: -------------------------------------------------------------------------------- 1 | 2 | icons_all_sprite 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/img/run.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/img/shorter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {API_URLS, RUNTIME_CONFIG} from './config'; 2 | import ExecutableCode from './executable-code'; 3 | import {waitForNode} from './utils'; 4 | import { 5 | default as discoursePreviewPanelHandler, 6 | Selectors as DiscourseSelectors 7 | } from './discourse-preview-panel-handler'; 8 | // support IE11 9 | import {polyfill} from "es6-promise"; 10 | 11 | polyfill(); 12 | 13 | 14 | /** 15 | * @typedef {Object} options 16 | * @property {string} server 17 | * @property {Function} onChange 18 | * @property {Function} onTestPassed 19 | * @property {Function} onTestFailed 20 | * @property {Function} onRun 21 | * @property {Function} onError 22 | * @property {Function} onConsoleOpen 23 | * @property {Function} onConsoleClose 24 | * @property {Function} callBack 25 | * 26 | * @param {string} selector 27 | * @param {Object} options 28 | * @return {Promise>} 29 | */ 30 | export default function create(selector, options = {}) { 31 | API_URLS.server = options.server || API_URLS.server; 32 | return ExecutableCode.create(selector, options); 33 | } 34 | 35 | // Backwards compatibility, should be removed in next major release 36 | create.default = create; 37 | 38 | /** 39 | * Initialize Kotlin playground for Discourse platform 40 | * @param {string} selector 41 | * @param {Object} options 42 | * @return {Promise>} 43 | */ 44 | create.discourse = function (selector, options) { 45 | discoursePreviewPanelHandler(); 46 | return create(selector, options); 47 | }; 48 | 49 | // Auto initialization via data-selector "); 170 | } 171 | if ( 172 | !isWasmRelated(targetPlatform) && 173 | targetPlatform !== TargetPlatforms.SWIFT_EXPORT 174 | ) { 175 | for (let lib of jsLibs) { 176 | iframeDoc.write(""); 177 | } 178 | if (targetPlatform === TargetPlatforms.JS_IR) { 179 | iframeDoc.write(``); 180 | } else { 181 | iframeDoc.write(``); 182 | } 183 | } 184 | if (targetPlatform === TargetPlatforms.COMPOSE_WASM) { 185 | 186 | const skikoStdlib = fetch(API_URLS.RESOURCE_VERSIONS(),{ 187 | method: 'GET' 188 | }).then(response => response.json()) 189 | .then(versions => { 190 | const skikoVersion = versions["skiko"]; 191 | 192 | const skikoExports = fetch(API_URLS.SKIKO_MJS(skikoVersion), { 193 | method: 'GET', 194 | headers: { 195 | 'Content-Type': 'text/javascript', 196 | } 197 | }).then(script => script.text()) 198 | .then(script => script.replace( 199 | "new URL(\"skiko.wasm\",import.meta.url).href", 200 | `'${API_URLS.SKIKO_WASM(skikoVersion)}'` 201 | )) 202 | .then(skikoCode => 203 | executeJs( 204 | this.iframe.contentWindow, 205 | skikoCode, 206 | )) 207 | .then(skikoExports => fixedSkikoExports(skikoExports)) 208 | 209 | const stdlibVersion = versions["stdlib"]; 210 | 211 | const stdlibExports = fetch(API_URLS.STDLIB_MJS(stdlibVersion), { 212 | method: 'GET', 213 | headers: { 214 | 'Content-Type': 'text/javascript', 215 | } 216 | }).then(script => script.text()) 217 | .then(script => 218 | // necessary to load stdlib.wasm before its initialization to parallelize 219 | // language=JavaScript 220 | (`const stdlibWasm = fetch('${API_URLS.STDLIB_WASM(stdlibVersion)}');\n` + script).replace( 221 | "fetch(new URL('./stdlib_master.wasm',import.meta.url).href)", 222 | "stdlibWasm" 223 | ).replace( 224 | "(extends) => { return { extends }; }", 225 | "(extends_) => { return { extends_ }; }" 226 | )) 227 | .then(stdlibCode => 228 | executeWasmCodeWithSkiko( 229 | this.iframe.contentWindow, 230 | stdlibCode, 231 | ) 232 | ) 233 | 234 | return Promise.all([skikoExports, stdlibExports]) 235 | }) 236 | 237 | this.stdlibExports = skikoStdlib 238 | .then(async ([skikoExportsResult, stdlibExportsResult]) => { 239 | return [ 240 | await stdlibExportsResult.instantiate({ 241 | "./skiko.mjs": skikoExportsResult 242 | }), 243 | stdlibExportsResult 244 | ] 245 | } 246 | ) 247 | .then(([stdlibResult, outputResult]) => { 248 | return { 249 | "stdlib": stdlibResult.exports, 250 | "output": outputResult.bufferedOutput 251 | } 252 | } 253 | ) 254 | 255 | this.iframe.height = "1000" 256 | iframeDoc.write(``); 257 | } 258 | iframeDoc.write(''); 259 | iframeDoc.close(); 260 | } 261 | } 262 | 263 | function fixedSkikoExports(skikoExports) { 264 | return { 265 | ...skikoExports, 266 | org_jetbrains_skia_Bitmap__1nGetPixmap: function () { 267 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 268 | }, 269 | org_jetbrains_skia_Bitmap__1nIsVolatile: function () { 270 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 271 | }, 272 | org_jetbrains_skia_Bitmap__1nSetVolatile: function () { 273 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 274 | }, 275 | org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer: function () { 276 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 277 | }, 278 | org_jetbrains_skia_TextBlobBuilderRunHandler__1nMake: function () { 279 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 280 | }, 281 | org_jetbrains_skia_TextBlobBuilderRunHandler__1nMakeBlob: function () { 282 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 283 | }, 284 | org_jetbrains_skia_svg_SVGCanvasKt__1nMake: function () { 285 | console.log("org_jetbrains_skia_TextBlobBuilderRunHandler__1nGetFinalizer") 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/js-executor/index.scss: -------------------------------------------------------------------------------- 1 | .k2js-iframe { 2 | display: none; 3 | } 4 | iframe { 5 | border: none; 6 | background: white; 7 | width: 100%; 8 | z-index: 10 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/crosslink.ts: -------------------------------------------------------------------------------- 1 | import { compressToBase64 } from 'lz-string'; 2 | 3 | import { getTargetById, TargetPlatformsKeys } from '../utils/platforms'; 4 | 5 | import { 6 | escapeRegExp, 7 | MARK_PLACEHOLDER_CLOSE, 8 | MARK_PLACEHOLDER_OPEN, 9 | SAMPLE_END, 10 | SAMPLE_START, 11 | } from '../utils/escape'; 12 | 13 | type LinkOptions = { 14 | targetPlatform?: TargetPlatformsKeys | Lowercase; 15 | compilerVersion?: string; 16 | }; 17 | 18 | /** 19 | * Assign the project to an employee. 20 | * @param {Object} code - The employee who is responsible for the project. 21 | * @param {Object} options - The employee who is responsible for the project. 22 | * @param {string} options.targetPlatform - The name of the employee. 23 | * @param {string} options.compilerVersion - The employee's department. 24 | */ 25 | export function generateCrosslink(code: string, options?: LinkOptions) { 26 | const opts: { code: string } & LinkOptions = { 27 | code: code 28 | .replace(new RegExp(escapeRegExp(MARK_PLACEHOLDER_OPEN), 'g'), '') 29 | .replace(new RegExp(escapeRegExp(MARK_PLACEHOLDER_CLOSE), 'g'), '') 30 | .replace(new RegExp(escapeRegExp(SAMPLE_START), 'g'), '') 31 | .replace(new RegExp(escapeRegExp(SAMPLE_END), 'g'), ''), 32 | }; 33 | 34 | if (options && options.targetPlatform) { 35 | const target = 36 | options.targetPlatform && getTargetById(options.targetPlatform); 37 | if (!target) throw new Error('Invalid target platform'); 38 | opts.targetPlatform = options.targetPlatform; 39 | } 40 | 41 | if (options && options.compilerVersion) 42 | opts.compilerVersion = options.compilerVersion; 43 | 44 | return `https://play.kotlinlang.org/editor/v1/${encodeURIComponent( 45 | compressToBase64(JSON.stringify(opts)), 46 | )}`; 47 | } 48 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~codemirror/lib/codemirror.css"; 2 | @import "~codemirror/theme/idea.css"; 3 | @import "~codemirror/theme/darcula.css"; 4 | @import "~codemirror/addon/scroll/simplescrollbars.css"; 5 | 6 | $font-family-mono: 'Liberation Mono', Consolas, Menlo, Courier, monospace; 7 | 8 | /** 9 | Darcula theme variables 10 | */ 11 | $darcula-background-color: #2B2B2B; 12 | $darcula-console-color: #313336; 13 | 14 | /** 15 | WebTeam UI colors 16 | */ 17 | $wt-color-tomato: #ec5424; 18 | $wt-color-seaweed: #4dbb5f; 19 | $wt-color-silver: #afb1b3; 20 | $wt-color-dove: #696969; 21 | $wt-color-code: #3c3f43; 22 | $warning-color: rgb(254, 255, 222); 23 | $wt-color-athens: #eaeaec; 24 | $wt-color-azure: #167dff; 25 | 26 | .executable-fragment-wrapper { 27 | --playground-code-output-padding: 10px; 28 | 29 | margin-bottom: 35px; 30 | position: relative; 31 | 32 | &_gutter { 33 | .CodeMirror { 34 | overflow: hidden; 35 | } 36 | } 37 | } 38 | 39 | .executable-fragment-wrapper__shorter { 40 | &, &:hover, &:active { 41 | box-shadow: 0 0 0 transparent; 42 | outline: 0 none transparent; 43 | text-shadow: 0 0 0 transparent; 44 | border: 1px solid #eaeaec; 45 | background: #fff url("img/shorter.svg") center center no-repeat; 46 | border-radius: 10px; 47 | position: absolute; 48 | height: 19px; 49 | width: 31px; 50 | left: 50%; 51 | transform: translate(-50%, -50%); 52 | z-index: 10; 53 | cursor: pointer; 54 | text-indent: -99999px; 55 | 56 | .theme-darcula-wrapper & { 57 | background-color: $darcula-background-color; 58 | border-color: #fff; 59 | } 60 | } 61 | } 62 | 63 | .executable-fragment { 64 | border: 1px solid $wt-color-athens; 65 | border-bottom-width: 0; 66 | } 67 | 68 | .executable-fragment.darcula { 69 | border: 1px solid gray; 70 | border-bottom-width: 0; 71 | background-color: $darcula-console-color; 72 | } 73 | 74 | .hidden-dependency { 75 | display: none; 76 | } 77 | 78 | .CodeMirror { 79 | height: auto; 80 | font-size: 12px; 81 | 82 | pre { 83 | line-height: 16.8px; 84 | margin-bottom: 0 !important; 85 | } 86 | 87 | .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { 88 | display: none; 89 | } 90 | 91 | &:hover { 92 | .CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { 93 | display: block; 94 | background: $wt-color-silver; 95 | } 96 | } 97 | } 98 | 99 | .CodeMirror-lines { 100 | padding: 0; 101 | margin: 12px 0; 102 | } 103 | 104 | .CodeMirror-gutter { 105 | height: auto; 106 | } 107 | 108 | .CodeMirror { 109 | line-height: 1.4; 110 | font-family: menlo, consolas, monospace; 111 | font-size: 12px; 112 | } 113 | 114 | .CodeMirror-linenumber { 115 | min-width: 0; 116 | text-align: center; 117 | } 118 | 119 | .CodeMirror-linebackground.unmodifiable-line { 120 | background-color: $wt-color-athens; 121 | } 122 | 123 | .CodeMirror-linebackground.unmodifiable-line-dark { 124 | background-color: $wt-color-code; 125 | } 126 | 127 | .markPlaceholder { 128 | border-top: 1px solid $wt-color-azure; 129 | border-bottom: 1px solid $wt-color-azure; 130 | } 131 | 132 | .markPlaceholder-start { 133 | border-left: 1px solid $wt-color-azure; 134 | } 135 | 136 | .markPlaceholder-end { 137 | border-right: 1px solid $wt-color-azure; 138 | } 139 | 140 | .run-button { 141 | position: absolute; 142 | z-index: 10; 143 | right: 5px; 144 | top: 5px; 145 | height: 20px; 146 | width: 16px; 147 | cursor: pointer; 148 | background-size: cover; 149 | background: url("img/run.svg") no-repeat; 150 | 151 | &._disabled { 152 | cursor: default; 153 | opacity: 0.5; 154 | } 155 | } 156 | 157 | .loader { 158 | position: relative; 159 | width: 15px; 160 | height: 15px; 161 | margin: 0 auto; 162 | border-radius: 50%; 163 | text-indent: -9999em; 164 | color: #161616; 165 | font-size: 8px; 166 | opacity: .7; 167 | animation: loader 1s infinite ease-in-out; 168 | animation-delay: 0.16s; 169 | } 170 | 171 | .loader.darcula { 172 | opacity: 1; 173 | color: #696969; 174 | } 175 | 176 | .loader::before { 177 | left: -3.5em; 178 | animation-delay: 0s; 179 | position: absolute; 180 | width: 15px; 181 | border-radius: 50%; 182 | height: 15px; 183 | content: ''; 184 | animation: loader 1s infinite ease-in-out; 185 | } 186 | 187 | .loader::after { 188 | left: 3.5em; 189 | animation-delay: 0.32s; 190 | position: absolute; 191 | border-radius: 50%; 192 | width: 15px; 193 | height: 15px; 194 | content: ''; 195 | animation: loader 1s infinite ease-in-out; 196 | } 197 | 198 | @keyframes loader { 199 | 100% { 200 | box-shadow: 0 2.5em 0 -1.3em; 201 | } 202 | 80% { 203 | box-shadow: 0 2.5em 0 -1.3em; 204 | } 205 | 0% { 206 | box-shadow: 0 2.5em 0 -1.3em; 207 | } 208 | 40% { 209 | box-shadow: 0 2.5em 0 0; 210 | } 211 | } 212 | 213 | .cm__ERROR { 214 | color: $wt-color-tomato !important; 215 | } 216 | 217 | .cm__IMPORT { 218 | text-decoration: underline; 219 | padding-bottom: 2px; 220 | } 221 | 222 | .errors-and-warnings-gutter { 223 | width: 16px; 224 | } 225 | 226 | .ERRORgutter { 227 | height: 13px; 228 | width: 13px; 229 | margin-top: 2px; 230 | margin-left: 2px; 231 | background: url("https://try.kotlinlang.org/static/images/icons_all_sprite.svg") no-repeat -150px -500px; 232 | } 233 | 234 | .WARNINGgutter { 235 | height: 13px; 236 | width: 13px; 237 | margin-top: 2px; 238 | margin-left: 2px; 239 | background: url("https://try.kotlinlang.org/static/images/icons_all_sprite.svg") no-repeat -150px -600px; 240 | } 241 | 242 | .cm__red_wavy_line { 243 | background: url("https://try.kotlinlang.org/static/images/wavyline-red.gif") repeat-x 100% 100%; 244 | padding-bottom: 2px; 245 | } 246 | 247 | .cm__green_wavy_line { 248 | background: url("https://try.kotlinlang.org/static/images/wavyline-green.gif") repeat-x 100% 100%; 249 | padding-bottom: 2px; 250 | } 251 | 252 | .js-code-output-executor { 253 | border-top: 1px solid $wt-color-athens; 254 | position: relative; 255 | } 256 | 257 | .js-code-output-executor.darcula { 258 | border-top: 1px solid grey; 259 | } 260 | 261 | .output-wrapper { 262 | flex-direction: column; 263 | display: flex; 264 | border-bottom: 1px solid $wt-color-athens; 265 | min-height: 60px; 266 | font-size: 14px; 267 | background-color: white; 268 | } 269 | 270 | .output-wrapper.darcula { 271 | background-color: $darcula-console-color; 272 | color: $wt-color-silver; 273 | } 274 | 275 | .code-output { 276 | flex-grow: 1; 277 | font-family: $font-family-mono; 278 | overflow: auto; 279 | padding-left: var(--playground-code-output-padding, 10px); 280 | padding-top: 15px; 281 | } 282 | 283 | .standard-output.darcula { 284 | color: $wt-color-silver; 285 | } 286 | 287 | .error-output { 288 | white-space: pre-wrap; 289 | color: $wt-color-tomato; 290 | min-height: 1.4em; 291 | margin: 0; 292 | vertical-align: top; 293 | } 294 | 295 | .error-output.darcula { 296 | color: $wt-color-tomato; 297 | } 298 | 299 | .standard-output { 300 | white-space: pre; 301 | color: #000; 302 | min-height: 1.4em; 303 | margin: 0; 304 | vertical-align: top; 305 | } 306 | 307 | .test-passed { 308 | white-space: pre; 309 | color: $wt-color-seaweed; 310 | min-height: 1.4em; 311 | vertical-align: top; 312 | } 313 | 314 | .console-close { 315 | position: absolute; 316 | right: 0; 317 | width: 16px; 318 | height: 16px; 319 | background: $wt-color-silver url('img/close-console-icon.svg') no-repeat; 320 | } 321 | 322 | .console-close.darcula { 323 | background: url('img/close-console-icon.svg'); 324 | } 325 | 326 | .console-close:hover { 327 | background-color: $wt-color-dove; 328 | } 329 | 330 | .console-close.darcula:hover { 331 | background-color: $wt-color-tomato; 332 | } 333 | 334 | .test-fail, .test-error { 335 | color: $wt-color-tomato; 336 | min-height: 1.4em; 337 | vertical-align: top; 338 | } 339 | 340 | .console-block { 341 | display: flex; 342 | } 343 | 344 | .console-icon { 345 | margin-top: 2px; 346 | padding-right: 20px; 347 | width: 15px; 348 | height: 15px; 349 | } 350 | 351 | .console-icon.attention { 352 | background: url('img/attention.svg') no-repeat; 353 | } 354 | 355 | .console-icon.passed { 356 | background: url('img/ok.svg') no-repeat; 357 | } 358 | 359 | .console-icon.fail { 360 | background: url('img/fail.svg') no-repeat; 361 | } 362 | 363 | .console-icon.error { 364 | background: url('img/fail.svg') no-repeat; 365 | } 366 | 367 | .test-time { 368 | float: right; 369 | font-size: 10px; 370 | color: $wt-color-silver; 371 | margin-right: 20px; 372 | margin-top: -14px; 373 | } 374 | 375 | .stacktrace-element { 376 | margin-left: 20px; 377 | 378 | .stacktrace-link { 379 | color: $wt-color-azure; 380 | cursor: pointer; 381 | &:hover { 382 | text-decoration: underline; 383 | } 384 | } 385 | } 386 | 387 | .CodeMirror-foldgutter { 388 | position: absolute; 389 | width: 100%; 390 | } 391 | 392 | .CodeMirror-foldgutter-folded { 393 | width: 100%; 394 | background: white; 395 | } 396 | 397 | .fold-button { 398 | position: absolute; 399 | height: 19px; 400 | width: 31px; 401 | top: 0; 402 | left: 50%; 403 | transform: translate(-50%, -50%); 404 | z-index: 10; 405 | cursor: pointer; 406 | background: transparent url('img/button_open.svg?fill=%23d4d4d4&selector=.data-fill') no-repeat; 407 | &._hover { 408 | background-image: url('img/button_open.svg?fill=%23f68322&selector=.data-fill'); 409 | } 410 | } 411 | 412 | .fold-button.darcula { 413 | background: url('img/button_open_darcula.svg') no-repeat; 414 | &._hover { 415 | background-image: url('img/button_open_darcula.svg?fill=%23f68322&selector=.data-fill'); 416 | } 417 | } 418 | 419 | ._unfolded { 420 | .fold-button { 421 | background-image: url('img/button_close.svg?fill=%23d4d4d4&selector=.data-fill'); 422 | &._hover { 423 | background-image: url('img/button_close.svg?fill=%23f68322&selector=.data-fill'); 424 | } 425 | } 426 | .fold-button.darcula { 427 | background: url('img/button_close_darcula.svg') no-repeat; 428 | &._hover { 429 | background-image: url('img/button_close_darcula.svg?fill=%23f68322&selector=.data-fill'); 430 | } 431 | } 432 | } 433 | 434 | .code-area { 435 | position: relative; 436 | } 437 | 438 | .compiler-info { 439 | &, & a { 440 | font-size: 10px; 441 | color: $wt-color-silver; 442 | } 443 | 444 | display: flex; 445 | justify-content: flex-end; 446 | 447 | padding-top: 5px; 448 | 449 | position: absolute; 450 | right: 0; 451 | left: 0; 452 | 453 | span { 454 | margin-left: 15px; 455 | } 456 | 457 | &_crosslink { 458 | justify-content: space-between; 459 | } 460 | 461 | &__open-editor { 462 | flex: 1 1 0; 463 | } 464 | 465 | &__target, &__version { 466 | white-space: nowrap; 467 | } 468 | } 469 | 470 | .CodeMirror-hints { 471 | padding-left: 0 !important; 472 | border: 1px solid $wt-color-silver; 473 | border-radius: 4px; 474 | list-style: none; 475 | position: absolute; 476 | background-color: #f7f7f7; 477 | overflow-y: hidden; 478 | z-index: 10; 479 | max-height: 20em; 480 | box-shadow: 2px 3px 5px rgba(0, 0, 0, .2); 481 | } 482 | 483 | .CodeMirror-hint { 484 | cursor: pointer; 485 | display: flex; 486 | align-items: center; 487 | padding-right: 5px; 488 | padding-left: 5px; 489 | } 490 | 491 | li.CodeMirror-hint-active { 492 | background-color: #d8d8d8; 493 | color: #000000; 494 | } 495 | 496 | li.CodeMirror-hint-active .name { 497 | overflow: auto; 498 | white-space: normal; 499 | } 500 | 501 | .CodeMirror-hint .name { 502 | margin-right: 20px; 503 | max-width: 60ch; 504 | overflow: hidden; 505 | white-space: nowrap; 506 | text-overflow: ellipsis; 507 | } 508 | 509 | li.CodeMirror-hint-active .tail { 510 | overflow: auto; 511 | white-space: normal; 512 | } 513 | 514 | .CodeMirror-hint .tail { 515 | margin-left: auto; 516 | margin-right: 5px; 517 | max-width: 30ch; 518 | overflow: hidden; 519 | white-space: nowrap; 520 | text-overflow: ellipsis; 521 | } 522 | 523 | .CodeMirror-hint .icon { 524 | height: 16px; 525 | width: 16px; 526 | margin-right: 5px; 527 | } 528 | 529 | .icon { 530 | background: url("https://try.kotlinlang.org/static/images/icons_all_sprite.svg"); 531 | } 532 | 533 | .icon.class { 534 | background: url("https://try.kotlinlang.org/static/images/completion_class.svg"); 535 | } 536 | 537 | .icon.package { 538 | background: url("https://try.kotlinlang.org/static/images/completion_package.svg"); 539 | } 540 | 541 | .icon.method { 542 | background: url("https://try.kotlinlang.org/static/images/completion_method.svg"); 543 | } 544 | 545 | .icon.genericValue { 546 | background: url("https://try.kotlinlang.org/static/images/completion_generic.svg") 547 | } 548 | 549 | .icon.property { 550 | background: url("https://try.kotlinlang.org/static/images/completion_property.svg"); 551 | } 552 | 553 | div[label]:hover:after { 554 | content: attr(label); 555 | padding: 0.25rem 0.5rem; 556 | white-space: pre; 557 | margin-top: -1rem; 558 | display: inline-flex; 559 | background: $warning-color; 560 | border-radius: 3px; 561 | margin-left: 1rem; 562 | color: black; 563 | border: 1px solid $wt-color-code; 564 | } 565 | 566 | .code-output .CodeMirror { 567 | margin-left: calc(-1 * var(--playground-code-output-padding)); 568 | padding-left: var(--playground-code-output-padding); 569 | } 570 | -------------------------------------------------------------------------------- /src/utils/escape.js: -------------------------------------------------------------------------------- 1 | export const SAMPLE_START = '//sampleStart'; 2 | export const SAMPLE_END = '//sampleEnd'; 3 | 4 | export const MARK_PLACEHOLDER_OPEN = "[mark]"; 5 | export const MARK_PLACEHOLDER_CLOSE = "[/mark]"; 6 | 7 | 8 | /** 9 | * Use instead of @escape-string-regexp 10 | */ 11 | 12 | export /*#__PURE__*/ function escapeRegExp(str) { 13 | return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 14 | } 15 | 16 | /** 17 | * Unescape special characters from string 18 | * @param string 19 | * @returns {string} 20 | */ 21 | export /*#__PURE__*/ function unEscapeString(string) { 22 | const tagsToReplace = { 23 | "<": "&lt;", 24 | ">": "&gt;", 25 | "&": "&", 26 | " ": "%20" 27 | }; 28 | let unEscapedString = string; 29 | Object.keys(tagsToReplace).forEach(function (key) { 30 | unEscapedString = unEscapedString.replace(new RegExp(tagsToReplace[key], 'g'), key) 31 | }); 32 | return unEscapedString 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'deepmerge'; 2 | import defaultConfig from '../config'; 3 | 4 | /** 5 | * Codemirror themes. 6 | * @type {{DARCULA: string, DEFAULT: string}} 7 | */ 8 | export const THEMES = { 9 | DARCULA: "darcula", 10 | IDEA: "idea", 11 | DEFAULT: "default" 12 | }; 13 | 14 | export const SAMPLE_START = '//sampleStart'; 15 | export const SAMPLE_END = '//sampleEnd'; 16 | 17 | export const MARK_PLACEHOLDER_OPEN = "[mark]"; 18 | export const MARK_PLACEHOLDER_CLOSE = "[/mark]"; 19 | 20 | /** 21 | * CodeMirror readonly tag 22 | * @type {string} 23 | */ 24 | export const READ_ONLY_TAG = 'nocursor'; 25 | 26 | /** 27 | * @param {*} arrayLike 28 | * @return {Array} 29 | */ 30 | export function arrayFrom(arrayLike) { 31 | return Array.prototype.slice.call(arrayLike, 0); 32 | } 33 | 34 | /** 35 | * Convert first letter of string in upper case` 36 | * @param string 37 | * @returns {string} 38 | */ 39 | export function capitalize(string) { 40 | if (typeof string !== 'string') return ''; 41 | return string.charAt(0).toUpperCase() + string.slice(1); 42 | } 43 | 44 | /** 45 | * Convert dashed string to camelCase` 46 | * @param string 47 | * @returns {string} 48 | */ 49 | export function dashToCamel(string) { 50 | return string 51 | .split('-') 52 | .map((el, i) => { 53 | if (!i) return el; 54 | return capitalize(el); 55 | }) 56 | .join(''); 57 | } 58 | 59 | /** 60 | * @param {Element} element 61 | * @param {boolean} mergeWithDefaults 62 | * @return {Object} 63 | */ 64 | export function getConfigFromElement(element, mergeWithDefaults = false) { 65 | if (!element || !element.attributes) { 66 | return {}; 67 | } 68 | 69 | const attrs = arrayFrom(element.attributes) 70 | .reduce((acc, {name, value}) => { 71 | if (name.indexOf('data-') === -1) return acc; 72 | 73 | const className = dashToCamel(name.replace('data-', '')); 74 | acc[className] = value; 75 | return acc; 76 | }, {}); 77 | 78 | return mergeWithDefaults 79 | ? merge.all([defaultConfig, attrs || {}]) 80 | : attrs; 81 | } 82 | 83 | /** 84 | * @return {HTMLScriptElement|null} 85 | */ 86 | export function getCurrentScript() { 87 | const scripts = document.getElementsByTagName('script'); 88 | return scripts[scripts.length - 1] || null; 89 | } 90 | 91 | /** 92 | * @param {Element} newNode 93 | * @param {Element} referenceNode 94 | */ 95 | export function insertAfter(newNode, referenceNode) { 96 | referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); 97 | } 98 | 99 | /** 100 | * Convert all `<` and `>` to `<` and `>` 101 | * @param string 102 | * @returns {*} 103 | */ 104 | export function processingHtmlBrackets(string) { 105 | const tagsToReplace = { 106 | "<": "<", 107 | ">": ">" 108 | }; 109 | let unEscapedString = string; 110 | Object.keys(tagsToReplace).forEach(function (key) { 111 | unEscapedString = unEscapedString.replace(new RegExp(tagsToReplace[key], 'g'), key) 112 | }); 113 | return unEscapedString 114 | } 115 | 116 | /** 117 | * convert all `<` and `>` to `<` and `>` 118 | * @param string 119 | * @returns {*} 120 | */ 121 | export function convertToHtmlTag(string) { 122 | const tagsToReplace = { 123 | "<": "&lt;", 124 | ">": "&gt;", 125 | }; 126 | let unEscapedString = string; 127 | Object.keys(tagsToReplace).forEach(function (key) { 128 | unEscapedString = unEscapedString.replace(new RegExp(tagsToReplace[key], 'g'), key) 129 | }); 130 | return unEscapedString 131 | } 132 | 133 | /** 134 | * Getting count of lines 135 | * @param string 136 | * @returns {number} 137 | */ 138 | export function countLines(string) { 139 | return (string.match(/\n/g) || []).length; 140 | } 141 | 142 | /** 143 | * Find and replace whitespaces from either the beginning or the end of the string. 144 | * @param string 145 | * @returns {string} 146 | */ 147 | export function replaceWhiteSpaces(string) { 148 | return string.replace(/^\s+|\s+$/g, ''); 149 | } 150 | 151 | /** 152 | * @param {string} selector 153 | * @param {Function} callback 154 | */ 155 | export function waitForNode(selector, callback) { 156 | const interval = setInterval(() => { 157 | const node = document.querySelector(selector); 158 | if (node) { 159 | clearInterval(interval); 160 | callback(node); 161 | } 162 | }, 100); 163 | } 164 | 165 | /** 166 | * Check that string consists only of blanks. 167 | * @param line 168 | * @returns {boolean} 169 | */ 170 | export function isEmpty(line) { 171 | return /^\s*$/.test(line); 172 | } 173 | -------------------------------------------------------------------------------- /src/utils/platforms/TargetPlatform.ts: -------------------------------------------------------------------------------- 1 | export default class TargetPlatform { 2 | id: string; 3 | printableName: string; 4 | 5 | constructor(id: string, printableName: string) { 6 | this.id = id; 7 | this.printableName = printableName; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/platforms/TargetPlatforms.ts: -------------------------------------------------------------------------------- 1 | import TargetPlatform from './TargetPlatform'; 2 | 3 | export const TargetPlatforms = { 4 | JS: new TargetPlatform('js', 'JavaScript'), 5 | JS_IR: new TargetPlatform('js-ir', 'JavaScript IR'), 6 | WASM: new TargetPlatform('wasm', 'Wasm'), 7 | COMPOSE_WASM: new TargetPlatform('compose-wasm', 'Compose Wasm'), 8 | JAVA: new TargetPlatform('java', 'JVM'), 9 | JUNIT: new TargetPlatform('junit', 'JUnit'), 10 | CANVAS: new TargetPlatform('canvas', 'JavaScript(canvas)'), 11 | SWIFT_EXPORT: new TargetPlatform('swift-export', 'Swift export'), 12 | } as const; 13 | 14 | export type TargetPlatformsKeys = keyof typeof TargetPlatforms; 15 | -------------------------------------------------------------------------------- /src/utils/platforms/index.ts: -------------------------------------------------------------------------------- 1 | import TargetPlatform from './TargetPlatform'; 2 | import { TargetPlatforms } from './TargetPlatforms'; 3 | import { isKeyOfObject } from '../types'; 4 | 5 | export function getTargetById(id?: string | null) { 6 | const key = id && id.toUpperCase().replace(/-/g, '_'); 7 | 8 | return isKeyOfObject(key, TargetPlatforms) ? TargetPlatforms[key] : null; 9 | } 10 | 11 | export function isJavaRelated(platform: TargetPlatform) { 12 | return ( 13 | platform === TargetPlatforms.JAVA || platform === TargetPlatforms.JUNIT 14 | ); 15 | } 16 | 17 | export function isJsRelated(platform: TargetPlatform) { 18 | return ( 19 | platform === TargetPlatforms.JS || 20 | platform === TargetPlatforms.JS_IR || 21 | platform === TargetPlatforms.CANVAS || 22 | platform === TargetPlatforms.SWIFT_EXPORT 23 | ); 24 | } 25 | 26 | export function isWasmRelated(platform: TargetPlatform) { 27 | return ( 28 | platform === TargetPlatforms.WASM || 29 | platform === TargetPlatforms.COMPOSE_WASM 30 | ); 31 | } 32 | 33 | export * from './TargetPlatforms'; 34 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export function isKeyOfObject( 2 | key: string | number | symbol, 3 | obj: T, 4 | ): key is keyof T { 5 | return key in obj; 6 | } 7 | -------------------------------------------------------------------------------- /src/view/completion-view.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from "../utils" 2 | const IMPORT_NAME = 'import'; 3 | const NO_LINE_NUMBER = -1; 4 | 5 | /** 6 | * Class for drawing own autocompletion view 7 | */ 8 | class CompletionView { 9 | 10 | constructor(completion) { 11 | this.completion = completion; 12 | } 13 | 14 | /** 15 | * Implementation of replacing text after choosing completion. 16 | * 17 | * @param elt - node element 18 | * @param data 19 | * @param cur 20 | */ 21 | render(elt, data, cur) { 22 | let icon = document.createElement('div'); 23 | let text = document.createElement('div'); 24 | let tail = document.createElement('div'); 25 | icon.setAttribute("class", "icon " + this.completion.icon); 26 | text.setAttribute("class", "name"); 27 | tail.setAttribute("class", "tail"); 28 | text.textContent = this.completion.displayText; 29 | tail.textContent = this.completion.tail; 30 | elt.appendChild(icon); 31 | elt.appendChild(text); 32 | elt.appendChild(tail); 33 | } 34 | 35 | /** 36 | * Render own styles when autocomplete displays. 37 | * 38 | * @param mirror - codemirror instance 39 | * @param self 40 | * @param data 41 | */ 42 | hint(mirror, self, data) { 43 | if (!this.completion[IMPORT_NAME] || this.completion.hasOtherImports) { 44 | this.completeText(mirror, this.completion.text); 45 | } else { 46 | this.addImport(mirror); 47 | } 48 | } 49 | 50 | completeText(mirror, text) { 51 | let cur = mirror.getCursor(); 52 | let token = mirror.getTokenAt(cur); 53 | let from = {line: cur.line, ch: token.start}; 54 | let to = {line: cur.line, ch: token.end}; 55 | const currentSymbol = token.string.trim(); 56 | if ([".", "", "(", ":"].includes(currentSymbol)) { 57 | mirror.replaceRange(text, to) 58 | } else { 59 | /* 60 | Replace string with $ in string in case=> 61 | val world = "world" 62 | println("Hello $world) 63 | 64 | Plain string => cursorInStringIndex = -1 65 | completionText will be equals result.text 66 | */ 67 | let cursorInStringIndex = cur.ch - token.start; 68 | let sentence$index = currentSymbol.substring(0, cursorInStringIndex).lastIndexOf('$'); 69 | let firstSentence = currentSymbol.substring(0, sentence$index + 1); 70 | let completionText = firstSentence + text + currentSymbol.substring(cursorInStringIndex, token.string.length); 71 | mirror.replaceRange(completionText, from, to); 72 | mirror.setCursor(cur.line, token.start + sentence$index + this.completion.text.length + 1); 73 | if (completionText.endsWith('(')) { 74 | mirror.replaceRange(")", {line: cur.line, ch: token.start + this.completion.text.length}); 75 | mirror.execCommand("goCharLeft"); 76 | } 77 | } 78 | } 79 | 80 | addImport(mirror) { 81 | const {packageLine, importLine} = this.findPackageLineAndFirstImportLine(mirror); 82 | let importText = "import " + this.completion[IMPORT_NAME] + "\n"; 83 | 84 | // if there are other imports => insert before them 85 | if (importLine !== NO_LINE_NUMBER) { 86 | mirror.replaceRange(importText, {line: importLine, ch: 0}); 87 | return; 88 | } 89 | 90 | if (packageLine !== NO_LINE_NUMBER) { 91 | importText = "\n" + importText; 92 | } 93 | let nextPackageLine = packageLine + 1; 94 | if (!isEmpty(mirror.getLine(nextPackageLine))) { 95 | importText += "\n"; 96 | } 97 | mirror.replaceRange(importText, {line: nextPackageLine, ch: 0}); 98 | 99 | let parts = this.completion.text.split('.'); 100 | let completionText = parts[parts.length - 1]; 101 | this.completeText(mirror, completionText); 102 | } 103 | 104 | findPackageLineAndFirstImportLine(mirror) { 105 | let packageLine = NO_LINE_NUMBER; 106 | let importLine = NO_LINE_NUMBER; 107 | let textLines = mirror.getValue().split("\n"); 108 | for(let i = 0; i < textLines.length; ++i) { 109 | let line = textLines[i]; 110 | if (/^\s*package /.test(line)) { 111 | packageLine = i; 112 | } else if (/^\s*import /.test(line)) { 113 | importLine = i; 114 | break; 115 | } else if (!isEmpty(line)) { 116 | break; 117 | } 118 | } 119 | return {packageLine, importLine}; 120 | } 121 | } 122 | 123 | export default CompletionView 124 | -------------------------------------------------------------------------------- /src/view/output-view.js: -------------------------------------------------------------------------------- 1 | import {arrayFrom, convertToHtmlTag, processingHtmlBrackets} from "../utils"; 2 | import isEmptyObject from "is-empty-object" 3 | import escapeHtml from "escape-html" 4 | 5 | 6 | const ACCESS_CONTROL_EXCEPTION = "java.security.AccessControlException"; 7 | const SECURITY_MESSAGE = "Access control exception due to security reasons in web playground"; 8 | const UNHANDLED_JS_EXCEPTION = "Unhandled JavaScript exception"; 9 | const NO_TEST_FOUND = "No tests methods are found"; 10 | const ANGLE_BRACKETS_LEFT_HTML = "<"; 11 | const ANGLE_BRACKETS_RIGHT_HTML = ">"; 12 | 13 | const TEST_STATUS = { 14 | FAIL : { value: "FAIL", text: "Fail" }, 15 | ERROR: { value: "ERROR", text: "Error" }, 16 | PASSED : { value: "OK", text: "Passed" } 17 | }; 18 | 19 | const BUG_FLAG = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}BUG${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`; 20 | const BUG_REPORT_MESSAGE = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}Hey! It seems you just found a bug! \uD83D\uDC1E\n` + 21 | `Please click here to submit it ` + 22 | `to the issue tracker and one day we fix it, hopefully \uD83D\uDE09\n` + 23 | `✅ Don't forget to attach code to the issue${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}\n`; 24 | 25 | export function processJVMOutput(output, theme) { 26 | let processedOutput = processingHtmlBrackets(output); // don't need to escape `&` 27 | return processedOutput 28 | .split(BUG_FLAG).join(BUG_REPORT_MESSAGE) 29 | .split(`${ANGLE_BRACKETS_LEFT_HTML}outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) 30 | .split(`${ANGLE_BRACKETS_LEFT_HTML}/outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join("") 31 | .split(`${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) 32 | .split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(""); 33 | } 34 | 35 | export function processJUnitResults(data, onTestPassed, onTestFailed) { 36 | let result = ""; 37 | let totalTime = 0; 38 | let passed = true; 39 | if (isEmptyObject(data)) return NO_TEST_FOUND; 40 | for (let testClass in data) { 41 | let listOfResults = arrayFrom(data[testClass]); 42 | result += listOfResults.reduce((previousTest, currentTest) => { 43 | totalTime = totalTime + (currentTest.executionTime / 1000); 44 | if (currentTest.status === TEST_STATUS.ERROR.value || currentTest.status === TEST_STATUS.FAIL.value) passed = false; 45 | switch (currentTest.status) { 46 | case TEST_STATUS.FAIL.value: 47 | return previousTest + buildOutputTestLine(TEST_STATUS.FAIL.text, currentTest.methodName, currentTest.comparisonFailure.message); 48 | case TEST_STATUS.ERROR.value: 49 | return previousTest + buildOutputTestLine(TEST_STATUS.ERROR.text, currentTest.methodName, currentTest.exception.message); 50 | case TEST_STATUS.PASSED.value: 51 | return previousTest + buildOutputTestLine(TEST_STATUS.PASSED.text, currentTest.methodName, ""); 52 | } 53 | }, ""); 54 | } 55 | if (passed && onTestPassed) onTestPassed(); 56 | if (!passed && onTestFailed) onTestFailed(); 57 | let testTime = `
Total test time: ${totalTime}s
`; 58 | return testTime + result; 59 | } 60 | 61 | function buildOutputTestLine(status, method, message) { 62 | return ` 63 |
64 | 65 |
${status}: ${method}${message ? ': ' + convertToHtmlTag(message) : ''}
66 |
67 | `; 68 | } 69 | 70 | export function processErrors(errors, theme) { 71 | return errors 72 | .reduce((acc, currentValue) => { 73 | return acc + `
74 | 75 |
${escapeHtml(currentValue.message)}
76 |
` 77 | } 78 | , ""); 79 | } 80 | 81 | /** 82 | * Check the security exception in the exception tree. 83 | * @param exception - json object that describes an exception 84 | * @returns {Object} add default security message to exception object if security exception is found 85 | */ 86 | export function findSecurityException(exception) { 87 | let currentException = exception; 88 | while (currentException != null) { 89 | if (currentException.fullName === ACCESS_CONTROL_EXCEPTION) { 90 | return getSecurityException(currentException); 91 | } 92 | currentException = currentException.cause; 93 | } 94 | return exception; 95 | } 96 | 97 | export function getExceptionCauses(exception) { 98 | if (exception.cause !== undefined && exception.cause != null) { 99 | return [exception.cause].concat(getExceptionCauses(exception.cause)) 100 | } else { 101 | return [] 102 | } 103 | } 104 | 105 | export function showJsException(exception) { 106 | console && console.error(exception); 107 | 108 | if (exception.stack != null) { 109 | let userStackTrace = exception.stack.toString().substr(0, exception.stack.toString().indexOf("at eval ()")); 110 | return `${UNHANDLED_JS_EXCEPTION}: ${exception.message} \n ${userStackTrace}`; 111 | } else { 112 | return UNHANDLED_JS_EXCEPTION; 113 | } 114 | } 115 | 116 | /** 117 | * Override exception message: append default security message. 118 | * Cut stack trace array - use only last stack trace element 119 | * @param exception - json object that describes an exception 120 | * @returns {Object} exception with default security message 121 | */ 122 | function getSecurityException(exception) { 123 | if (exception.stackTrace != null) { 124 | if (exception.message != null) { 125 | exception.message = `${SECURITY_MESSAGE}: \n ` + exception.message; 126 | } else { 127 | exception.message = SECURITY_MESSAGE 128 | } 129 | exception.stackTrace = exception.stackTrace.slice(exception.stackTrace.length - 1) 130 | } 131 | return exception 132 | } 133 | -------------------------------------------------------------------------------- /src/webdemo-api.js: -------------------------------------------------------------------------------- 1 | import { fetch } from 'whatwg-fetch'; 2 | import { API_URLS } from './config'; 3 | import flatten from 'flatten'; 4 | import { isWasmRelated, TargetPlatforms } from './utils/platforms'; 5 | import { 6 | findSecurityException, 7 | getExceptionCauses, 8 | processErrors, 9 | processJUnitResults, 10 | processJVMOutput, 11 | } from './view/output-view'; 12 | 13 | /** 14 | * @typedef {Object} KotlinVersion 15 | * @property {string} version 16 | * @property {string} build 17 | * @property {boolean} obsolete 18 | * @property {boolean} latestStable 19 | * @property {boolean} hasScriptJar 20 | * @property {string|null} stdlibVersion 21 | */ 22 | 23 | const CACHE = { 24 | compilerVersions: null, 25 | }; 26 | const DEFAULT_FILE_NAME = 'File.kt'; 27 | 28 | export default class WebDemoApi { 29 | /** 30 | * @return {Promise>} 31 | */ 32 | static getCompilerVersions() { 33 | if (!CACHE.compilerVersions) { 34 | CACHE.compilerVersions = fetch(API_URLS.VERSIONS) 35 | .then((response) => response.json()) 36 | .catch(() => (CACHE.compilerVersions = null)); 37 | } 38 | 39 | return CACHE.compilerVersions; 40 | } 41 | 42 | /** 43 | * Request on translation Kotlin code to JS code 44 | * 45 | * @param code - string 46 | * @param compilerVersion - string kotlin compiler 47 | * @param platform - TargetPlatform 48 | * @param args - command line arguments 49 | * @param hiddenDependencies - read only additional files 50 | * @returns {*|PromiseLike|Promise} 51 | */ 52 | static translateKotlinToJs( 53 | code, 54 | compilerVersion, 55 | platform, 56 | args, 57 | hiddenDependencies, 58 | ) { 59 | const MINIMAL_VERSION_IR = '1.5.0'; 60 | const MINIMAL_VERSION_WASM = '1.9.0'; 61 | const MINIMAL_VERSION_SWIFT_EXPORT = '2.0.0'; 62 | 63 | if ( 64 | platform === TargetPlatforms.JS_IR && 65 | compilerVersion < MINIMAL_VERSION_IR 66 | ) { 67 | return Promise.resolve({ 68 | output: '', 69 | errors: [ 70 | { 71 | severity: 'ERROR', 72 | message: `JS IR compiler backend accessible only since ${MINIMAL_VERSION_IR} version`, 73 | }, 74 | ], 75 | jsCode: '', 76 | }); 77 | } 78 | 79 | if (isWasmRelated(platform) && compilerVersion < MINIMAL_VERSION_WASM) { 80 | return Promise.resolve({ 81 | output: '', 82 | errors: [ 83 | { 84 | severity: 'ERROR', 85 | message: `Wasm compiler backend accessible only since ${MINIMAL_VERSION_WASM} version`, 86 | }, 87 | ], 88 | jsCode: '', 89 | }); 90 | } 91 | 92 | if ( 93 | platform === TargetPlatforms.SWIFT_EXPORT && 94 | compilerVersion < MINIMAL_VERSION_SWIFT_EXPORT 95 | ) { 96 | return Promise.resolve({ 97 | output: '', 98 | errors: [ 99 | { 100 | severity: 'ERROR', 101 | message: `Swift export accessible only since ${MINIMAL_VERSION_SWIFT_EXPORT} version`, 102 | }, 103 | ], 104 | jsCode: '', 105 | }); 106 | } 107 | 108 | return executeCode( 109 | API_URLS.COMPILE(platform, compilerVersion), 110 | code, 111 | compilerVersion, 112 | platform, 113 | args, 114 | hiddenDependencies, 115 | ).then(function (data) { 116 | let output = ''; 117 | let errorsAndWarnings = flatten(Object.values(data.errors)); 118 | return { 119 | output: output, 120 | errors: errorsAndWarnings, 121 | jsCode: data.jsCode, 122 | wasm: data.wasm, 123 | }; 124 | }); 125 | } 126 | 127 | /** 128 | * Request on execute Kotlin code. 129 | * 130 | * @param code - string 131 | * @param compilerVersion - string kotlin compiler 132 | * @param platform - TargetPlatform 133 | * @param args - command line arguments 134 | * @param theme - theme of editor 135 | * @param onTestPassed - function will call after test's passed 136 | * @param onTestFailed - function will call after test's failed 137 | * @param hiddenDependencies - read only additional files 138 | * @returns {*|PromiseLike|Promise} 139 | */ 140 | static executeKotlinCode( 141 | code, 142 | compilerVersion, 143 | platform, 144 | args, 145 | theme, 146 | hiddenDependencies, 147 | onTestPassed, 148 | onTestFailed, 149 | ) { 150 | return executeCode( 151 | API_URLS.COMPILE(platform, compilerVersion), 152 | code, 153 | compilerVersion, 154 | platform, 155 | args, 156 | hiddenDependencies, 157 | ).then(function (data) { 158 | let output = ''; 159 | let errorsAndWarnings = flatten(Object.values(data.errors)); 160 | let errors = errorsAndWarnings.filter( 161 | (error) => error.severity === 'ERROR', 162 | ); 163 | if (errors.length > 0) { 164 | output = processErrors(errors, theme); 165 | } else { 166 | switch (platform) { 167 | case TargetPlatforms.JAVA: 168 | if (data.text) output = processJVMOutput(data.text, theme); 169 | break; 170 | case TargetPlatforms.JUNIT: 171 | data.testResults 172 | ? (output = processJUnitResults( 173 | data.testResults, 174 | onTestPassed, 175 | onTestFailed, 176 | )) 177 | : (output = processJVMOutput(data.text || '', theme)); 178 | break; 179 | } 180 | } 181 | let exceptions = null; 182 | if (data.exception != null) { 183 | exceptions = findSecurityException(data.exception); 184 | exceptions.causes = getExceptionCauses(exceptions); 185 | exceptions.cause = undefined; 186 | } 187 | return { 188 | errors: errorsAndWarnings, 189 | output: output, 190 | exception: exceptions, 191 | }; 192 | }); 193 | } 194 | 195 | /** 196 | * Request for getting list of different completion proposals 197 | * 198 | * @param code - string code 199 | * @param cursor - cursor position in code 200 | * @param compilerVersion - string kotlin compiler 201 | * @param hiddenDependencies - read only additional files 202 | * @param platform - kotlin platform {@see TargetPlatform} 203 | * @param callback 204 | */ 205 | static getAutoCompletion( 206 | code, 207 | cursor, 208 | compilerVersion, 209 | platform, 210 | hiddenDependencies, 211 | callback, 212 | ) { 213 | const { line, ch, ...options } = cursor; 214 | const url = API_URLS.COMPLETE(compilerVersion) + `?line=${line}&ch=${ch}`; 215 | executeCode( 216 | url, 217 | code, 218 | compilerVersion, 219 | platform, 220 | '', 221 | hiddenDependencies, 222 | options, 223 | ).then((data) => { 224 | callback(data); 225 | }); 226 | } 227 | 228 | /** 229 | * Request for getting errors of current file 230 | * 231 | * @param code - string code 232 | * @param compilerVersion - string kotlin compiler 233 | * @param platform - kotlin platform {@see TargetPlatform} 234 | * @param hiddenDependencies - read only additional files 235 | * @return {*|PromiseLike|Promise} 236 | */ 237 | static getHighlight(code, compilerVersion, platform, hiddenDependencies) { 238 | return executeCode( 239 | API_URLS.HIGHLIGHT(compilerVersion), 240 | code, 241 | compilerVersion, 242 | platform, 243 | '', 244 | hiddenDependencies, 245 | ).then((data) => data[DEFAULT_FILE_NAME] || []); 246 | } 247 | } 248 | 249 | function executeCode( 250 | url, 251 | code, 252 | compilerVersion, 253 | targetPlatform, 254 | args, 255 | hiddenDependencies, 256 | options, 257 | ) { 258 | const files = [buildFileObject(code, DEFAULT_FILE_NAME)].concat( 259 | hiddenDependencies.map((file, index) => 260 | buildFileObject(file, `hiddenDependency${index}.kt`), 261 | ), 262 | ); 263 | 264 | const body = { 265 | args, 266 | files, 267 | confType: targetPlatform.id, 268 | ...(options || {}), 269 | }; 270 | 271 | return fetch(url, { 272 | method: 'POST', 273 | body: JSON.stringify(body), 274 | headers: { 275 | 'Content-Type': 'application/json; charset=utf-8', 276 | }, 277 | }).then((response) => response.json()); 278 | } 279 | 280 | /** 281 | * 282 | * Build file object. 283 | * @param code - string code 284 | * @param fileName - name of file 285 | * @returns {{name: string, text: string, publicId: string}} - file object 286 | */ 287 | function buildFileObject(code, fileName) { 288 | return { 289 | name: fileName, 290 | text: code, 291 | publicId: '', 292 | }; 293 | } 294 | -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-highlight-only-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-highlight-only-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-2.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-3.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-4.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/dev/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/dev/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-highlight-only-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-highlight-only-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-2.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-3.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/basics.e2e.ts-basics-simple-usage-4.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Chrome/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-highlight-only-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-highlight-only-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-2.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-3.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/basics.e2e.ts-basics-simple-usage-4.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_linux/Desktop-Firefox/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_linux/Desktop-Firefox/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-highlight-only-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-highlight-only-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-2.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-3.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/basics.e2e.ts-basics-simple-usage-4.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/crosslink.e2e.ts-open-in-playground-with-crosslink-defined-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-JS-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/__screenshots__/github_mac/Desktop-Safari/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains/kotlin-playground/991bb0630de59abb875dcbfc03777f6c61de9692/tests/__screenshots__/github_mac/Desktop-Safari/wasm-compatibility.e2e.ts-WASM-platform-with-moduleId-in-output-WASM-1-9-server-response-1.png -------------------------------------------------------------------------------- /tests/basics.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator, Page, test } from '@playwright/test'; 2 | 3 | import { checkRunCase, prepareNetwork, printlnCode, toPostData } from './utils'; 4 | import { gotoHtmlWidget } from './utils/server/playground'; 5 | 6 | import { 7 | OPEN_EDITOR_SELECTOR, 8 | RUN_SELECTOR, 9 | TARGET_SELECTOR, 10 | VERSION_SELECTOR, 11 | WIDGET_SELECTOR, 12 | } from './utils/selectors'; 13 | 14 | import { closeButton, replaceStringInEditor } from './utils/interactions'; 15 | import { checkEditorView, checkScreenshot } from './utils/screenshots'; 16 | 17 | test.describe('basics', () => { 18 | test.beforeEach(async ({ page, baseURL }) => { 19 | await prepareNetwork(page, baseURL); // offline mode 20 | }); 21 | 22 | test('highlight only', async ({ page }) => { 23 | await gotoHtmlWidget( 24 | page, 25 | { selector: 'code' }, 26 | `${printlnCode('Hello, world!')}`, 27 | ); 28 | 29 | const editor = page.locator(WIDGET_SELECTOR); 30 | 31 | await expect(page.locator('code')).not.toBeVisible(); // original node hided 32 | await expect(editor).toHaveCount(1); // playground loaded 33 | await expect(editor.locator(OPEN_EDITOR_SELECTOR)).not.toBeVisible(); // open on play-link 34 | await expect(editor.locator(TARGET_SELECTOR)).not.toBeVisible(); // default target JVN 35 | await expect(editor.locator(VERSION_SELECTOR)).not.toBeVisible(); // latest version marker 36 | await expect(editor.locator(RUN_SELECTOR)).not.toBeVisible(); 37 | 38 | // Take screen fullpage, for sure original node should be invisible 39 | await checkScreenshot(page.locator('body'), 'initial view is correct'); 40 | }); 41 | 42 | test('simple usage', async ({ page }) => { 43 | await gotoHtmlWidget( 44 | page, 45 | { selector: 'code' }, 46 | `

before

47 | ${printlnCode('Hello, world!')} 48 |

after

`, 49 | ); 50 | 51 | const editor = page.locator(WIDGET_SELECTOR); 52 | await expect(editor).toHaveCount(1); // playground loaded 53 | await expect(page.locator('code')).not.toBeVisible(); // original node hided 54 | 55 | // editor on correct DOM position 56 | await expect(editor.locator('xpath=preceding-sibling::p[1]')).toHaveText( 57 | 'before', 58 | ); 59 | await expect(editor.locator('xpath=following-sibling::p[1]')).toHaveText( 60 | 'after', 61 | ); 62 | 63 | await expect(editor.locator(RUN_SELECTOR)).toHaveCount(1); // run button exists 64 | await expect(editor.locator(OPEN_EDITOR_SELECTOR)).toHaveCount(1); // open on play-link exists 65 | await expect(editor.locator(TARGET_SELECTOR)).toHaveText('Target: JVM'); // default target JVM 66 | await expect(editor.locator(VERSION_SELECTOR)).toHaveText( 67 | 'Running on v.1.9.20', // latest version marker 68 | ); 69 | 70 | // Take screen fullpage, for sure original node should be invisible 71 | await checkScreenshot(page.locator('body'), 'initial view is correct'); 72 | 73 | // run with default source 74 | await checkPrintlnCase(page, editor, 'Hello, world!'); 75 | 76 | // click close button 77 | await closeButton(editor); 78 | await checkEditorView(editor, 'console closed'); 79 | 80 | // Edit and run 81 | await replaceStringInEditor(page, editor, 'Hello, world!', 'edited'); 82 | await checkPrintlnCase(page, editor, 'edited'); 83 | }); 84 | 85 | test('user init widget', async ({ page }) => { 86 | await gotoHtmlWidget(page, `${printlnCode('Hello, world!')}`); 87 | 88 | const editor = page.locator(WIDGET_SELECTOR); 89 | 90 | // doesn't rendered by default 91 | await expect(editor).toHaveCount(0); 92 | 93 | //language=javascript 94 | const content = `(() => { 95 | const KotlinPlayground = window.KotlinPlayground; 96 | KotlinPlayground('code'); 97 | })();`; 98 | 99 | await page.addScriptTag({ content }); 100 | 101 | // playground loaded 102 | await expect(editor).toHaveCount(1); 103 | }); 104 | }); 105 | 106 | export function checkPrintlnCase( 107 | page: Page, 108 | editor: Locator, 109 | text: string, 110 | platform: string = 'java', 111 | ) { 112 | const source = toPostData(printlnCode(text)); 113 | const postData = `{"args":"","files":[{"name":"File.kt","text":"${source}","publicId":""}],"confType":"${platform}"}`; 114 | 115 | const serverOutput = { 116 | json: Object.freeze({ 117 | errors: { 'File.kt': [] }, 118 | exception: null, 119 | text: `${text}\n`, 120 | }), 121 | }; 122 | 123 | return checkRunCase(page, editor, postData, serverOutput); 124 | } 125 | -------------------------------------------------------------------------------- /tests/crosslink.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page, test } from '@playwright/test'; 2 | import { gotoHtmlWidget } from './utils/server/playground'; 3 | import { prepareNetwork, printlnCode, toHtmlAttributes } from './utils'; 4 | import { OPEN_EDITOR_SELECTOR, WIDGET_SELECTOR } from './utils/selectors'; 5 | import { checkEditorView } from './utils/screenshots'; 6 | 7 | test.describe('open in playground', () => { 8 | test.beforeEach(async ({ page, baseURL }) => { 9 | await prepareNetwork(page, baseURL); // offline mode 10 | }); 11 | 12 | test('default @external', async ({ page }) => { 13 | const code = printlnCode('Hello, world!'); 14 | await gotoHtmlWidget(page, { selector: 'code' }, `${code}`); 15 | const editor = page.locator(WIDGET_SELECTOR); 16 | 17 | const link = editor.locator(OPEN_EDITOR_SELECTOR).locator('a[href]'); 18 | 19 | const url = 20 | 'https://play.kotlinlang.org/editor/v1/N4Igxg9gJgpiBcIBmBXAdgAgLYEMCWaAFAJQbAA6mG1ADgE4EAuANkeSABIzPMQA0GAO4Q6zKAEJ2xStQC%2BIPiEY46AcxiMACsxyMkIrAhAArHADccC8BCw08zGHQBqjgM54IaIwEYAdAE5fACYABhBZIA%3D%3D'; 21 | 22 | await Promise.all([ 23 | expect(link).toHaveCount(1), 24 | expect(link).toHaveText('Open in Playground →'), 25 | expect(link).toHaveAttribute('target', '_blank'), 26 | expect(link).toHaveAttribute('rel', 'noopener noreferrer'), 27 | expect(link).toHaveAttribute('href', url), 28 | ]); 29 | 30 | const [newPage] = await Promise.all([ 31 | page.context().waitForEvent('page'), // open new page with target blank 32 | link.first().click(), 33 | ]); 34 | 35 | expect(newPage.url()).toEqual(url); // new page with correct url 36 | 37 | // wait until final page with editor 38 | await newPage.waitForURL((url) => 39 | url.toString().startsWith('https://play.kotlinlang.org/#'), 40 | ); 41 | 42 | // check it exists on page 43 | await expect(newPage.locator('body')).toContainText(code); 44 | 45 | await newPage.close(); 46 | }); 47 | 48 | test('with crosslink defined', ({ page }) => 49 | checkCrosslink( 50 | page, 51 | { 'data-crosslink': 'disabled' }, 52 | printlnCode('Hello, World'), 53 | )); 54 | 55 | test('no link for js-libs', ({ page }) => 56 | checkCrosslink( 57 | page, 58 | { 59 | 'data-target-platform': 'js', 60 | 'data-js-libs': 'https://somescript.js', 61 | }, 62 | printlnCode('Hello, World!'), 63 | )); 64 | 65 | test('no link for hidden deps', ({ page }) => 66 | checkCrosslink( 67 | page, 68 | {}, 69 | `${printlnCode('Hello, World!')} 70 | `, 73 | )); 74 | 75 | test('no link for highlight only', ({ page }) => 76 | checkCrosslink( 77 | page, 78 | { 'data-highlight-only': true }, 79 | printlnCode('Hello, World!'), 80 | false, // hidden crosslink=enabled too 81 | )); 82 | }); 83 | 84 | async function checkCrosslink( 85 | page: Page, 86 | options: Record, 87 | code: string, 88 | existWhenEnabled: boolean = true, 89 | ) { 90 | // ===== check original case ===== 91 | await checkCrosslinkStatus(page, options, code); 92 | // ===== check case with data-crosslink=enabled mode ===== 93 | const enabledOptions = { ...options, 'data-crosslink': 'enabled' }; 94 | await checkCrosslinkStatus(page, enabledOptions, code, existWhenEnabled); 95 | } 96 | 97 | async function checkCrosslinkStatus( 98 | page: Page, 99 | options: Record, 100 | code: string, 101 | exists: boolean = false, 102 | ) { 103 | await gotoHtmlWidget( 104 | page, 105 | { selector: 'code' }, 106 | `${code}`, 107 | ); 108 | 109 | const editor = page.locator(WIDGET_SELECTOR); 110 | 111 | await expect(editor).toHaveCount(1); 112 | await expect(editor.locator(OPEN_EDITOR_SELECTOR)).toHaveCount( 113 | exists ? 1 : 0, 114 | ); 115 | 116 | if (options['data-crosslink'] === 'disabled') 117 | await checkEditorView(editor, 'hided crosslink'); 118 | } 119 | -------------------------------------------------------------------------------- /tests/crosslink.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { promises as fs } from 'fs'; 3 | import { decompressFromBase64 } from 'lz-string'; 4 | 5 | const prefix = 'https://play.kotlinlang.org/editor/v1/' as const; 6 | 7 | test.describe('crosslink: library', () => { 8 | test('exports', async () => { 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const module = require('../dist/crosslink'); 11 | expect(module).toBeDefined(); 12 | 13 | const { generateCrosslink } = module; 14 | 15 | expect(typeof generateCrosslink).toEqual('function'); 16 | 17 | // Pass just codeWithSample 18 | checkLink(generateCrosslink('simple'), { code: 'simple' }); 19 | 20 | // Pass platforms with codeWithSample 21 | checkLink(generateCrosslink('platform', { targetPlatform: 'js-ir' }), { 22 | code: 'platform', 23 | targetPlatform: 'js-ir', 24 | }); 25 | 26 | // Invalid target 27 | expect(() => 28 | generateCrosslink('platform', { targetPlatform: 'NOT_A_PLATFORM' }), 29 | ).toThrow(); 30 | 31 | // Pass compilerVersion with codeWithSample 32 | checkLink(generateCrosslink('version', { compilerVersion: '1.5.21' }), { 33 | code: 'version', 34 | compilerVersion: '1.5.21', 35 | }); 36 | 37 | // Pass random with codeWithSample 38 | checkLink(generateCrosslink('random', { randomProperty: '1.5.21' }), { 39 | code: 'random', 40 | }); 41 | 42 | //language=kotlin 43 | const codeWithSample = `fun main(args: Array) { 44 | //sampleStart 45 | val (name, value) = Pair("Kitty", "Kiss") 46 | println(name) 47 | println(value) 48 | //sampleEnd 49 | }`; 50 | 51 | checkLink(generateCrosslink(codeWithSample), { 52 | //language=text 53 | code: `fun main(args: Array) { 54 | ${' '} 55 | val (name, value) = Pair("Kitty", "Kiss") 56 | println(name) 57 | println(value) 58 | ${' '} 59 | }`, 60 | }); 61 | 62 | //language=text 63 | const codeWithMark = 64 | 'fun containsEven(collection: Collection): Boolean = collection.any {[mark]TODO()[/mark]}'; 65 | 66 | checkLink(generateCrosslink(codeWithMark), { 67 | //language=kotlin 68 | code: 'fun containsEven(collection: Collection): Boolean = collection.any {TODO()}', 69 | }); 70 | }); 71 | 72 | test('definition', async () => { 73 | test.fixme(true, "The definition isn't implemented yet"); 74 | const stats = await fs.stat('../dist/crosslink.d.ts'); 75 | expect(stats.size).toBeGreaterThan(0); 76 | }); 77 | }); 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 80 | function checkLink(link: string, expected: Record) { 81 | expect(link.startsWith(prefix)).toBeTruthy(); 82 | 83 | const output = decodeURIComponent(link.substring(prefix.length)); 84 | const payload = JSON.parse(decompressFromBase64(output)); 85 | 86 | expect(payload).toEqual(expected); 87 | } 88 | -------------------------------------------------------------------------------- /tests/min-version.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { gotoHtmlWidget } from './utils/server/playground'; 3 | import { prepareNetwork, printlnCode } from './utils'; 4 | import { mockRunRequest, waitRunRequest } from './utils/mocks/compiler'; 5 | import { runButton } from './utils/interactions'; 6 | import { 7 | LOADER_SELECTOR, 8 | RESULT_SELECTOR, 9 | VERSION_SELECTOR, 10 | WIDGET_SELECTOR, 11 | } from './utils/selectors'; 12 | 13 | test.describe('Minimum compiler version', () => { 14 | test.beforeEach(async ({ page, baseURL }) => { 15 | await prepareNetwork(page, baseURL); // offline mode 16 | }); 17 | 18 | test('should select the future release version by default', async ({ 19 | page, 20 | }) => { 21 | await gotoHtmlWidget( 22 | page, 23 | { selector: 'code' }, 24 | /* language=html */ ` 25 | ${printlnCode( 26 | 'Hello, world!', 27 | )} 28 | `, 29 | ); 30 | 31 | const editor = page.locator(WIDGET_SELECTOR); 32 | await expect(editor).toHaveCount(1); // playground loaded 33 | await expect(page.locator('code')).not.toBeVisible(); // original node hided 34 | await expect(editor.locator(VERSION_SELECTOR)).toHaveText( 35 | 'Running on v.2.0.0-alpha.3', // latest version marker 36 | ); 37 | 38 | const resolveRun = await mockRunRequest(page); 39 | 40 | const [request] = await Promise.all([ 41 | waitRunRequest(page), 42 | runButton(editor), 43 | ]); 44 | 45 | expect(request.url()).toContain('2.0.0-alpha.3'); 46 | 47 | await expect(editor.locator(LOADER_SELECTOR)).toBeVisible(); 48 | 49 | resolveRun({ 50 | json: Object.freeze({ 51 | errors: { 'File.kt': [] }, 52 | exception: null, 53 | text: 'run code - done!\n', 54 | }), 55 | }); 56 | 57 | await expect(editor.locator(RESULT_SELECTOR)).toBeVisible(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/restrictions.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page, test } from '@playwright/test'; 2 | 3 | import { readFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | 6 | import { gotoHtmlWidget } from './utils/server/playground'; 7 | 8 | import { RESULT_SELECTOR, WIDGET_SELECTOR } from './utils/selectors'; 9 | 10 | import { prepareNetwork, printlnCode } from './utils'; 11 | import { mockRunRequest, waitRunRequest } from './utils/mocks/compiler'; 12 | import { runButton } from './utils/interactions'; 13 | import { makeJSPrintCode } from './utils/mocks/wasm/result'; 14 | 15 | const WASM = JSON.parse( 16 | readFileSync(join(__dirname, 'utils/mocks/wasm/wasm.json'), 'utf-8'), 17 | ); 18 | 19 | const JS_IR = Object.freeze({ 20 | jsCode: makeJSPrintCode('Hello, world!'), 21 | errors: { 'File.kt': [] }, 22 | exception: null, 23 | text: 'Hello, world!\n', 24 | }); 25 | 26 | const OUTPUTS = Object.freeze({ 27 | 'js-ir': JS_IR, 28 | wasm: WASM, 29 | }); 30 | 31 | const VERSIONS = [ 32 | { version: '1.3.10' }, 33 | { version: '1.9.20', latestStable: true }, 34 | { version: '2.0.1' }, 35 | ] as const; 36 | 37 | test.describe('platform restrictions', () => { 38 | test.beforeEach(async ({ page, baseURL }) => { 39 | await prepareNetwork(page, baseURL, { 40 | versions: (route) => 41 | route.fulfill({ 42 | body: JSON.stringify(VERSIONS), 43 | }), 44 | }); // offline mode 45 | }); 46 | 47 | test('JS_IR for unsupported version', async ({ page }) => { 48 | await shouldFailedRun( 49 | page, 50 | 'js-ir', 51 | '1.3.10', 52 | 'JS IR compiler backend accessible only since 1.5.0 version', 53 | ); 54 | }); 55 | 56 | test('JS_IR for supported by minor version', async ({ page }) => { 57 | await shouldSuccessRun(page, 'js-ir', '1.9.0'); 58 | }); 59 | 60 | test('JS_IR for supported by major version', async ({ page }) => { 61 | await shouldSuccessRun(page, 'js-ir', '2.0.1'); 62 | }); 63 | 64 | test('WASM for unsupported version', async ({ page }) => { 65 | await shouldFailedRun( 66 | page, 67 | 'wasm', 68 | '1.3.10', 69 | 'Wasm compiler backend accessible only since 1.9.0 version', 70 | ); 71 | }); 72 | 73 | test('WASM for supported by minor version', async ({ page, browserName }) => { 74 | test.skip( 75 | browserName !== 'chromium', 76 | "WASM doesn't supported in this browser", 77 | ); 78 | await shouldSuccessRun(page, 'wasm', '1.9.0'); 79 | }); 80 | 81 | test('WASM for supported by major version', async ({ page, browserName }) => { 82 | test.skip( 83 | browserName !== 'chromium', 84 | "WASM doesn't supported in this browser", 85 | ); 86 | await shouldSuccessRun(page, 'wasm', '2.0.1'); 87 | }); 88 | }); 89 | 90 | async function shouldSuccessRun( 91 | page: Page, 92 | platform: keyof typeof OUTPUTS, 93 | version: string, 94 | ) { 95 | await gotoHtmlWidget( 96 | page, 97 | { selector: 'code', version: version }, 98 | /* language=html */ ` 99 | ${printlnCode( 100 | 'Hello, world!', 101 | )} 102 | `, 103 | ); 104 | 105 | const resolveRun = await mockRunRequest(page); 106 | 107 | const editor = page.locator(WIDGET_SELECTOR); 108 | 109 | await Promise.all([waitRunRequest(page), runButton(editor)]); 110 | 111 | resolveRun({ 112 | json: Object.freeze(OUTPUTS[platform]), 113 | }); 114 | 115 | // playground loaded 116 | await expect(editor.locator(RESULT_SELECTOR)).toBeVisible(); 117 | await expect(editor.locator(RESULT_SELECTOR)).toContainText('Hello, world!'); 118 | } 119 | 120 | async function shouldFailedRun( 121 | page: Page, 122 | platform: string, 123 | version: string, 124 | text: string, 125 | ) { 126 | await gotoHtmlWidget( 127 | page, 128 | { selector: 'code', version: version }, 129 | /* language=html */ ` 130 | ${printlnCode( 131 | 'Hello, world!', 132 | )} 133 | `, 134 | ); 135 | 136 | const editor = page.locator(WIDGET_SELECTOR); 137 | await runButton(editor); 138 | 139 | await expect(editor.locator(RESULT_SELECTOR)).toBeVisible(); 140 | await expect( 141 | editor.locator(RESULT_SELECTOR).locator('.test-fail'), 142 | ).toContainText(text); 143 | } 144 | -------------------------------------------------------------------------------- /tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserContext, expect, Locator, Page, Route } from '@playwright/test'; 2 | 3 | import { mockRunRequest, mockVersions, waitRunRequest } from './mocks/compiler'; 4 | import { LOADER_SELECTOR, RESULT_SELECTOR } from './selectors'; 5 | import { runButton } from './interactions'; 6 | import { checkEditorView } from './screenshots'; 7 | 8 | export type RouteFulfill = Parameters[0]; 9 | 10 | export async function refuseExternalUrls( 11 | page: Page | BrowserContext, 12 | baseURL: string, 13 | ) { 14 | const host = new URL(baseURL).host; 15 | 16 | const checkUrl = (url: URL) => url.host && url.host !== host; 17 | const onMatch = (route: Route) => route.abort('connectionrefused'); 18 | 19 | await page.route(checkUrl, onMatch); 20 | return () => page.unroute(checkUrl, onMatch); 21 | } 22 | 23 | export async function prepareNetwork( 24 | page: Page | BrowserContext, 25 | baseURL: string, 26 | options?: { 27 | versions: Parameters[1]; 28 | }, 29 | ) { 30 | const unRefuse = await refuseExternalUrls(page, baseURL); 31 | const unVersions = await mockVersions(page, options?.versions); 32 | 33 | return async () => { 34 | await unVersions(); 35 | await unRefuse(); 36 | }; 37 | } 38 | 39 | export function toPostData(code: string) { 40 | return code.replace(/\n/g, '\\n').replace(/"/g, '\\"'); 41 | } 42 | 43 | export function printlnCode(text: string) { 44 | //language=kotlin 45 | return `fun main() { 46 | println("${text}") 47 | }`; 48 | } 49 | 50 | export function toHtmlAttributes( 51 | options: Record, 52 | ) { 53 | return Object.entries(options) 54 | .filter(([, val]) => val) 55 | .map(([key, val]) => `${key}${typeof val === 'string' ? '=' + val : ''}`) 56 | .join(' '); 57 | } 58 | 59 | export async function checkRunCase( 60 | page: Page, 61 | editor: Locator, 62 | postData: string, 63 | serverOutput: RouteFulfill, 64 | ) { 65 | const resolveRun = await mockRunRequest(page); 66 | 67 | const [request] = await Promise.all([ 68 | waitRunRequest(page), 69 | runButton(editor), 70 | ]); 71 | 72 | expect(postData).toEqual(request.postData()); 73 | 74 | await expect(editor.locator(LOADER_SELECTOR)).toBeVisible(); 75 | // await expectScreenshot(editor, 'run code - loading!'); 76 | 77 | resolveRun(serverOutput); 78 | 79 | await expect(editor.locator(RESULT_SELECTOR)).toBeVisible(); 80 | await checkEditorView(editor, 'run code - done!'); 81 | } 82 | -------------------------------------------------------------------------------- /tests/utils/interactions.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator, Page } from '@playwright/test'; 2 | 3 | import { 4 | RUN_SELECTOR, 5 | CLOSE_SELECTOR, 6 | OUTPUT_SELECTOR, 7 | editorString, 8 | } from './selectors'; 9 | 10 | export async function putSelection(page: Page, length: number, value: string) { 11 | await setSelection(page, length); 12 | await page.keyboard.type(value); 13 | } 14 | 15 | export async function setSelection(page: Page, length: number) { 16 | await page.keyboard.down('Shift'); 17 | for (let i = 0; i < length; i++) await page.keyboard.press('ArrowRight'); 18 | await page.keyboard.up('Shift'); 19 | } 20 | 21 | export async function runButton(code: Locator) { 22 | await code.locator(RUN_SELECTOR).click(); 23 | 24 | await expect(code.locator(OUTPUT_SELECTOR)).toBeVisible(); 25 | } 26 | 27 | export async function closeButton(code: Locator) { 28 | await code.locator(CLOSE_SELECTOR).click(); 29 | 30 | await expect(code.locator(OUTPUT_SELECTOR)).toHaveCount(0); 31 | } 32 | 33 | export async function replaceStringInEditor( 34 | page: Page, 35 | code: Locator, 36 | oldValue: string, 37 | newValue: string, 38 | ) { 39 | const bounding = await code.locator(editorString(oldValue)).boundingBox(); 40 | 41 | await page.mouse.click(bounding.x + 1, bounding.y + 1, { button: 'left' }); 42 | await putSelection(page, oldValue.length + 2, `"${newValue}"`); 43 | } 44 | -------------------------------------------------------------------------------- /tests/utils/mocks/compiler.ts: -------------------------------------------------------------------------------- 1 | import { BrowserContext, Route, Request, Page } from '@playwright/test'; 2 | import { join } from 'path'; 3 | import { RouteFulfill } from '../index'; 4 | 5 | export const API_HOST = 'api.kotlinlang.org'; 6 | 7 | function defaultVersions(route: Route, req: Request) { 8 | if (req.method() !== 'GET') { 9 | return route.continue(); 10 | } 11 | 12 | return route.fulfill({ path: join(__dirname, 'versions.json') }); 13 | } 14 | 15 | export async function mockVersions( 16 | page: Page | BrowserContext, 17 | resp?: Parameters[1] | Parameters[1], 18 | ) { 19 | const checkUrl = (url: URL) => 20 | url.host === API_HOST && url.pathname.match(/^\/?\/versions$/) !== null; 21 | const onMatch = resp || defaultVersions; 22 | 23 | await page.route(checkUrl, onMatch); 24 | 25 | return () => page.unroute(checkUrl, onMatch); 26 | } 27 | 28 | function isRunRequest(url: URL | string) { 29 | const uri = url instanceof URL ? url : new URL(url); 30 | 31 | return ( 32 | uri.host === API_HOST && 33 | (uri.pathname.match( 34 | /^\/?\/api\/\d+\.\d+\.\d+(-[a-z]+.\d+)?\/compiler\/run$/, 35 | ) !== null || 36 | uri.pathname.match( 37 | /^\/?\/api\/\d+\.\d+\.\d+(-[a-z]+.\d+)?\/compiler\/translate$/, 38 | ) !== null) 39 | ); 40 | } 41 | 42 | export async function mockRunRequest(page: Page | BrowserContext) { 43 | let resolve: (value?: RouteFulfill) => void; 44 | 45 | const promise = new Promise((cb) => { 46 | resolve = cb; 47 | }); 48 | 49 | const checkUrl = (url: URL) => isRunRequest(url); 50 | const onMatch = (route: Route) => 51 | route.request().method() !== 'POST' 52 | ? route.continue() 53 | : promise.then((value) => route.fulfill(value)); 54 | 55 | await page.route(checkUrl, onMatch); 56 | 57 | return (value: RouteFulfill) => { 58 | resolve(value); 59 | page.unroute(checkUrl, onMatch); 60 | }; 61 | } 62 | 63 | export async function waitRunRequest(page: Page) { 64 | return page.waitForRequest( 65 | (req) => req.method() === 'POST' && isRunRequest(req.url()), 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /tests/utils/mocks/versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "1.2.71" 4 | }, 5 | { 6 | "version": "1.3.72" 7 | }, 8 | { 9 | "version": "1.4.30" 10 | }, 11 | { 12 | "version": "1.5.31" 13 | }, 14 | { 15 | "version": "1.6.21" 16 | }, 17 | { 18 | "version": "1.7.21" 19 | }, 20 | { 21 | "version": "1.8.21" 22 | }, 23 | { 24 | "version": "1.9.20", 25 | "latestStable": true 26 | }, 27 | { 28 | "version": "2.0.0-alpha.3" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /tests/utils/mocks/wasm-1.9/result.ts: -------------------------------------------------------------------------------- 1 | export function makeJSPrintCode(text: string) { 2 | return `var moduleId = function (_) {\n 'use strict';\n //region block: pre-declaration\n setMetadataFor(Unit, 'Unit', objectMeta);\n setMetadataFor(BaseOutput, 'BaseOutput', classMeta);\n setMetadataFor(NodeJsOutput, 'NodeJsOutput', classMeta, BaseOutput);\n setMetadataFor(BufferedOutput, 'BufferedOutput', classMeta, BaseOutput, VOID, BufferedOutput);\n setMetadataFor(BufferedOutputToConsoleLog, 'BufferedOutputToConsoleLog', classMeta, BufferedOutput, VOID, BufferedOutputToConsoleLog);\n //endregion\n function Unit() {\n }\n protoOf(Unit).toString = function () {\n return 'kotlin.Unit';\n };\n var Unit_instance;\n function Unit_getInstance() {\n return Unit_instance;\n }\n function get_output() {\n _init_properties_console_kt__rfg7jv();\n return output;\n }\n var output;\n function BaseOutput() {\n }\n function NodeJsOutput(outputStream) {\n BaseOutput.call(this);\n this.outputStream_1 = outputStream;\n }\n protoOf(NodeJsOutput).print_o1pwgy_k$ = function (message) {\n // Inline function 'kotlin.io.String' call\n var messageString = String(message);\n this.outputStream_1.write(messageString);\n };\n function BufferedOutputToConsoleLog() {\n BufferedOutput.call(this);\n }\n protoOf(BufferedOutputToConsoleLog).print_o1pwgy_k$ = function (message) {\n // Inline function 'kotlin.io.String' call\n var s = String(message);\n // Inline function 'kotlin.text.nativeLastIndexOf' call\n // Inline function 'kotlin.js.asDynamic' call\n var i = s.lastIndexOf('\\n', 0);\n if (i >= 0) {\n var tmp = this;\n var tmp_0 = this.buffer_1;\n // Inline function 'kotlin.text.substring' call\n // Inline function 'kotlin.js.asDynamic' call\n tmp.buffer_1 = tmp_0 + s.substring(0, i);\n this.flush_shahbo_k$();\n // Inline function 'kotlin.text.substring' call\n var this_0 = s;\n var startIndex = i + 1 | 0;\n // Inline function 'kotlin.js.asDynamic' call\n s = this_0.substring(startIndex);\n }\n this.buffer_1 = this.buffer_1 + s;\n };\n protoOf(BufferedOutputToConsoleLog).flush_shahbo_k$ = function () {\n console.log(this.buffer_1);\n this.buffer_1 = '';\n };\n function BufferedOutput() {\n BaseOutput.call(this);\n this.buffer_1 = '';\n }\n protoOf(BufferedOutput).print_o1pwgy_k$ = function (message) {\n var tmp = this;\n var tmp_0 = this.buffer_1;\n // Inline function 'kotlin.io.String' call\n tmp.buffer_1 = tmp_0 + String(message);\n };\n function print(message) {\n _init_properties_console_kt__rfg7jv();\n get_output().print_o1pwgy_k$(message);\n }\n var properties_initialized_console_kt_gll9dl;\n function _init_properties_console_kt__rfg7jv() {\n if (!properties_initialized_console_kt_gll9dl) {\n properties_initialized_console_kt_gll9dl = true;\n // Inline function 'kotlin.run' call\n // Inline function 'kotlin.contracts.contract' call\n // Inline function 'kotlin.io.output.' call\n var isNode = typeof process !== 'undefined' && process.versions && !!process.versions.node;\n output = isNode ? new NodeJsOutput(process.stdout) : new BufferedOutputToConsoleLog();\n }\n }\n function implement(interfaces) {\n var maxSize = 1;\n var masks = [];\n var inductionVariable = 0;\n var last = interfaces.length;\n while (inductionVariable < last) {\n var i = interfaces[inductionVariable];\n inductionVariable = inductionVariable + 1 | 0;\n var currentSize = maxSize;\n var tmp1_elvis_lhs = i.prototype.$imask$;\n var imask = tmp1_elvis_lhs == null ? i.$imask$ : tmp1_elvis_lhs;\n if (!(imask == null)) {\n masks.push(imask);\n currentSize = imask.length;\n }\n var iid = i.$metadata$.iid;\n var tmp;\n if (iid == null) {\n tmp = null;\n } else {\n // Inline function 'kotlin.let' call\n // Inline function 'kotlin.contracts.contract' call\n // Inline function 'kotlin.js.implement.' call\n tmp = bitMaskWith(iid);\n }\n var iidImask = tmp;\n if (!(iidImask == null)) {\n masks.push(iidImask);\n currentSize = Math.max(currentSize, iidImask.length);\n }\n if (currentSize > maxSize) {\n maxSize = currentSize;\n }\n }\n return compositeBitMask(maxSize, masks);\n }\n function bitMaskWith(activeBit) {\n var numberIndex = activeBit >> 5;\n var intArray = new Int32Array(numberIndex + 1 | 0);\n var positionInNumber = activeBit & 31;\n var numberWithSettledBit = 1 << positionInNumber;\n intArray[numberIndex] = intArray[numberIndex] | numberWithSettledBit;\n return intArray;\n }\n function compositeBitMask(capacity, masks) {\n var tmp = 0;\n var tmp_0 = new Int32Array(capacity);\n while (tmp < capacity) {\n var tmp_1 = tmp;\n var result = 0;\n var inductionVariable = 0;\n var last = masks.length;\n while (inductionVariable < last) {\n var mask = masks[inductionVariable];\n inductionVariable = inductionVariable + 1 | 0;\n if (tmp_1 < mask.length) {\n result = result | mask[tmp_1];\n }\n }\n tmp_0[tmp_1] = result;\n tmp = tmp + 1 | 0;\n }\n return tmp_0;\n }\n function protoOf(constructor) {\n return constructor.prototype;\n }\n function defineProp(obj, name, getter, setter) {\n return Object.defineProperty(obj, name, {configurable: true, get: getter, set: setter});\n }\n function objectCreate(proto) {\n return Object.create(proto);\n }\n function classMeta(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n return createMetadata('class', name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, null);\n }\n function createMetadata(kind, name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, iid) {\n var undef = VOID;\n return {kind: kind, simpleName: name, associatedObjectKey: associatedObjectKey, associatedObjects: associatedObjects, suspendArity: suspendArity, $kClass$: undef, defaultConstructor: defaultConstructor, iid: iid};\n }\n function setMetadataFor(ctor, name, metadataConstructor, parent, interfaces, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n if (!(parent == null)) {\n ctor.prototype = Object.create(parent.prototype);\n ctor.prototype.constructor = ctor;\n }\n var metadata = metadataConstructor(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity == null ? [] : suspendArity);\n ctor.$metadata$ = metadata;\n if (!(interfaces == null)) {\n var receiver = !(metadata.iid == null) ? ctor : ctor.prototype;\n receiver.$imask$ = implement(interfaces);\n }\n }\n function objectMeta(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n return createMetadata('object', name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, null);\n }\n function get_VOID() {\n _init_properties_void_kt__3zg9as();\n return VOID;\n }\n var VOID;\n var properties_initialized_void_kt_e4ret2;\n function _init_properties_void_kt__3zg9as() {\n if (!properties_initialized_void_kt_e4ret2) {\n properties_initialized_void_kt_e4ret2 = true;\n VOID = void 0;\n }\n }\n function main() {\n print('${text}');\n }\n //region block: init\n Unit_instance = new Unit();\n //endregion\nif (typeof get_output !== "undefined") {\n get_output();\n output = new BufferedOutput();\n _.output = get_output();\n}\n main();\n return _;\n}(typeof moduleId === 'undefined' ? {} : moduleId);\nmoduleId.output?.buffer_1;\n\n` as const; 3 | } 4 | -------------------------------------------------------------------------------- /tests/utils/mocks/wasm/result.ts: -------------------------------------------------------------------------------- 1 | export function makeJSPrintCode(text: string) { 2 | return `var playground = function (_) {\n 'use strict';\n //region block: pre-declaration\n setMetadataFor(Unit, 'Unit', objectMeta);\n setMetadataFor(BaseOutput, 'BaseOutput', classMeta);\n setMetadataFor(NodeJsOutput, 'NodeJsOutput', classMeta, BaseOutput);\n setMetadataFor(BufferedOutput, 'BufferedOutput', classMeta, BaseOutput, VOID, BufferedOutput);\n setMetadataFor(BufferedOutputToConsoleLog, 'BufferedOutputToConsoleLog', classMeta, BufferedOutput, VOID, BufferedOutputToConsoleLog);\n //endregion\n function Unit() {\n }\n protoOf(Unit).toString = function () {\n return 'kotlin.Unit';\n };\n var Unit_instance;\n function Unit_getInstance() {\n return Unit_instance;\n }\n function get_output() {\n _init_properties_console_kt__rfg7jv();\n return output;\n }\n var output;\n function BaseOutput() {\n }\n function NodeJsOutput(outputStream) {\n BaseOutput.call(this);\n this.outputStream_1 = outputStream;\n }\n protoOf(NodeJsOutput).print_o1pwgy_k$ = function (message) {\n // Inline function 'kotlin.io.String' call\n var messageString = String(message);\n this.outputStream_1.write(messageString);\n };\n function BufferedOutputToConsoleLog() {\n BufferedOutput.call(this);\n }\n protoOf(BufferedOutputToConsoleLog).print_o1pwgy_k$ = function (message) {\n // Inline function 'kotlin.io.String' call\n var s = String(message);\n // Inline function 'kotlin.text.nativeLastIndexOf' call\n // Inline function 'kotlin.js.asDynamic' call\n var i = s.lastIndexOf('\\n', 0);\n if (i >= 0) {\n var tmp = this;\n var tmp_0 = this.buffer_1;\n // Inline function 'kotlin.text.substring' call\n // Inline function 'kotlin.js.asDynamic' call\n tmp.buffer_1 = tmp_0 + s.substring(0, i);\n this.flush_shahbo_k$();\n // Inline function 'kotlin.text.substring' call\n var this_0 = s;\n var startIndex = i + 1 | 0;\n // Inline function 'kotlin.js.asDynamic' call\n s = this_0.substring(startIndex);\n }\n this.buffer_1 = this.buffer_1 + s;\n };\n protoOf(BufferedOutputToConsoleLog).flush_shahbo_k$ = function () {\n console.log(this.buffer_1);\n this.buffer_1 = '';\n };\n function BufferedOutput() {\n BaseOutput.call(this);\n this.buffer_1 = '';\n }\n protoOf(BufferedOutput).print_o1pwgy_k$ = function (message) {\n var tmp = this;\n var tmp_0 = this.buffer_1;\n // Inline function 'kotlin.io.String' call\n tmp.buffer_1 = tmp_0 + String(message);\n };\n function print(message) {\n _init_properties_console_kt__rfg7jv();\n get_output().print_o1pwgy_k$(message);\n }\n var properties_initialized_console_kt_gll9dl;\n function _init_properties_console_kt__rfg7jv() {\n if (!properties_initialized_console_kt_gll9dl) {\n properties_initialized_console_kt_gll9dl = true;\n // Inline function 'kotlin.run' call\n // Inline function 'kotlin.contracts.contract' call\n // Inline function 'kotlin.io.output.' call\n var isNode = typeof process !== 'undefined' && process.versions && !!process.versions.node;\n output = isNode ? new NodeJsOutput(process.stdout) : new BufferedOutputToConsoleLog();\n }\n }\n function implement(interfaces) {\n var maxSize = 1;\n var masks = [];\n var inductionVariable = 0;\n var last = interfaces.length;\n while (inductionVariable < last) {\n var i = interfaces[inductionVariable];\n inductionVariable = inductionVariable + 1 | 0;\n var currentSize = maxSize;\n var tmp1_elvis_lhs = i.prototype.$imask$;\n var imask = tmp1_elvis_lhs == null ? i.$imask$ : tmp1_elvis_lhs;\n if (!(imask == null)) {\n masks.push(imask);\n currentSize = imask.length;\n }\n var iid = i.$metadata$.iid;\n var tmp;\n if (iid == null) {\n tmp = null;\n } else {\n // Inline function 'kotlin.let' call\n // Inline function 'kotlin.contracts.contract' call\n // Inline function 'kotlin.js.implement.' call\n tmp = bitMaskWith(iid);\n }\n var iidImask = tmp;\n if (!(iidImask == null)) {\n masks.push(iidImask);\n currentSize = Math.max(currentSize, iidImask.length);\n }\n if (currentSize > maxSize) {\n maxSize = currentSize;\n }\n }\n return compositeBitMask(maxSize, masks);\n }\n function bitMaskWith(activeBit) {\n var numberIndex = activeBit >> 5;\n var intArray = new Int32Array(numberIndex + 1 | 0);\n var positionInNumber = activeBit & 31;\n var numberWithSettledBit = 1 << positionInNumber;\n intArray[numberIndex] = intArray[numberIndex] | numberWithSettledBit;\n return intArray;\n }\n function compositeBitMask(capacity, masks) {\n var tmp = 0;\n var tmp_0 = new Int32Array(capacity);\n while (tmp < capacity) {\n var tmp_1 = tmp;\n var result = 0;\n var inductionVariable = 0;\n var last = masks.length;\n while (inductionVariable < last) {\n var mask = masks[inductionVariable];\n inductionVariable = inductionVariable + 1 | 0;\n if (tmp_1 < mask.length) {\n result = result | mask[tmp_1];\n }\n }\n tmp_0[tmp_1] = result;\n tmp = tmp + 1 | 0;\n }\n return tmp_0;\n }\n function protoOf(constructor) {\n return constructor.prototype;\n }\n function defineProp(obj, name, getter, setter) {\n return Object.defineProperty(obj, name, {configurable: true, get: getter, set: setter});\n }\n function objectCreate(proto) {\n return Object.create(proto);\n }\n function classMeta(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n return createMetadata('class', name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, null);\n }\n function createMetadata(kind, name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, iid) {\n var undef = VOID;\n return {kind: kind, simpleName: name, associatedObjectKey: associatedObjectKey, associatedObjects: associatedObjects, suspendArity: suspendArity, $kClass$: undef, defaultConstructor: defaultConstructor, iid: iid};\n }\n function setMetadataFor(ctor, name, metadataConstructor, parent, interfaces, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n if (!(parent == null)) {\n ctor.prototype = Object.create(parent.prototype);\n ctor.prototype.constructor = ctor;\n }\n var metadata = metadataConstructor(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity == null ? [] : suspendArity);\n ctor.$metadata$ = metadata;\n if (!(interfaces == null)) {\n var receiver = !(metadata.iid == null) ? ctor : ctor.prototype;\n receiver.$imask$ = implement(interfaces);\n }\n }\n function objectMeta(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n return createMetadata('object', name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, null);\n }\n function get_VOID() {\n _init_properties_void_kt__3zg9as();\n return VOID;\n }\n var VOID;\n var properties_initialized_void_kt_e4ret2;\n function _init_properties_void_kt__3zg9as() {\n if (!properties_initialized_void_kt_e4ret2) {\n properties_initialized_void_kt_e4ret2 = true;\n VOID = void 0;\n }\n }\n function main() {\n print('${text}');\n }\n //region block: init\n Unit_instance = new Unit();\n //endregion\nif (typeof get_output !== "undefined") {\n get_output();\n output = new BufferedOutput();\n _.output = get_output();\n}\n main();\n return _;\n}(typeof playground === 'undefined' ? {} : playground);\nplayground.output?.buffer_1;\n\n` as const; 3 | } 4 | -------------------------------------------------------------------------------- /tests/utils/screenshots.ts: -------------------------------------------------------------------------------- 1 | import { expect, Locator } from '@playwright/test'; 2 | import { WIDGET_WRAPPER_SELECTOR } from './selectors'; 3 | 4 | export async function hideCursor(node: Locator, callback: () => Promise) { 5 | const cursor = node.locator('.CodeMirror-cursors'); 6 | // Cursor blinks all the time, it's failed test from time to time 7 | await cursor.evaluate((element) => (element.style.display = 'none')); 8 | await callback(); 9 | await cursor.evaluate((element) => (element.style.display = null)); 10 | } 11 | 12 | const MAX_DIFF_PIXEL_RATIO = 0.01; 13 | 14 | export function checkScreenshot(node: Locator, message: string) { 15 | return hideCursor(node, () => 16 | expect(node, message).toHaveScreenshot({ 17 | maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO, 18 | }), 19 | ); 20 | } 21 | 22 | export function checkEditorView(editor: Locator, message: string) { 23 | return hideCursor(editor, async () => { 24 | /* Add top/bottom margins for wrapper node */ 25 | const [boundingBox, margins] = await Promise.all([ 26 | editor.boundingBox(), 27 | editor.locator(`> ${WIDGET_WRAPPER_SELECTOR}`).evaluate((el) => ({ 28 | top: parseFloat( 29 | window.getComputedStyle(el).getPropertyValue('margin-top'), 30 | ), 31 | bottom: parseFloat( 32 | window.getComputedStyle(el).getPropertyValue('margin-bottom'), 33 | ), 34 | })), 35 | ]); 36 | 37 | const clip = { 38 | ...boundingBox, 39 | y: boundingBox.y - margins.top, 40 | height: boundingBox.height + margins.bottom, 41 | }; 42 | 43 | await expect(editor.page(), message).toHaveScreenshot({ 44 | clip, 45 | maxDiffPixelRatio: MAX_DIFF_PIXEL_RATIO, 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /tests/utils/selectors.ts: -------------------------------------------------------------------------------- 1 | export const WIDGET_WRAPPER_SELECTOR = '.executable-fragment-wrapper'; 2 | export const WIDGET_SELECTOR = `div:has(> ${WIDGET_WRAPPER_SELECTOR})`; 3 | export const RUN_SELECTOR = '.run-button'; 4 | export const CLOSE_SELECTOR = '.console-close'; 5 | export const OUTPUT_SELECTOR = '.output-wrapper'; 6 | export const LOADER_SELECTOR = `${OUTPUT_SELECTOR} > .loader`; 7 | export const RESULT_SELECTOR = `${OUTPUT_SELECTOR} > .code-output`; 8 | 9 | export const TARGET_SELECTOR = '.compiler-info__target'; 10 | export const OPEN_EDITOR_SELECTOR = '.compiler-info__open-editor'; 11 | export const VERSION_SELECTOR = '.compiler-info__version'; 12 | 13 | export function editorLine(number: number) { 14 | return `.CodeMirror-code .CodeMirror-line:nth-child(${number})`; 15 | } 16 | 17 | export function editorString(text: string) { 18 | return `.cm-string:text("${text}")`; 19 | } 20 | -------------------------------------------------------------------------------- /tests/utils/server/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const fs = require('fs').promises; 3 | const { join } = require('path'); 4 | const { getType } = require('mime'); 5 | const { decode } = require('querystring'); 6 | 7 | const port = 8000; 8 | 9 | async function sendFile(res, pathname, cb = null) { 10 | let content = await fs.readFile(join(__dirname, pathname)); 11 | if (cb) content = cb(content.toString('utf-8')); 12 | 13 | res.setHeader('content-type', getType(pathname)); 14 | res.writeHead(200); 15 | 16 | res.end(content); 17 | } 18 | 19 | async function readJSON(req) { 20 | return new Promise((resolve, reject) => { 21 | let body = ''; 22 | 23 | req.on('data', function (data) { 24 | body += data; 25 | }); 26 | req.on('end', function () { 27 | try { 28 | resolve(decode(body)); 29 | } catch (e) { 30 | reject(e); 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | async function requestListener(req, res) { 37 | try { 38 | let response = null; 39 | const path = new URL(req.url, `http://localhost:${port}`).pathname; 40 | 41 | if (req.method === 'GET' && path.startsWith('/blank.html')) 42 | response = Promise.resolve().then(() => { 43 | res.writeHead(200); 44 | res.end(''); 45 | }); 46 | 47 | if (req.method === 'POST' && path === '/playground.html') { 48 | const { script, body } = await readJSON(req); 49 | response = sendFile(res, './playground.html', (content) => 50 | content 51 | .replace( 52 | / data-script-config/, 53 | script ? decodeURIComponent(script) : '', 54 | ) 55 | .replace(//, body ? decodeURIComponent(body) : ''), 56 | ); 57 | } 58 | 59 | if (req.method === 'GET' && path.startsWith('/dist/')) 60 | response = sendFile(res, '../../../' + path.substring(1)); 61 | 62 | if (response !== null) { 63 | await response; 64 | return; 65 | } 66 | 67 | res.writeHead(404); 68 | res.end('Not found'); 69 | } catch (e) { 70 | res.writeHead(500); 71 | res.end('ERROR: ' + e.message); 72 | } 73 | } 74 | 75 | const server = http.createServer(requestListener); 76 | 77 | process.on('SIGINT', function () { 78 | process.exit(); 79 | }); 80 | 81 | server.listen(port); 82 | -------------------------------------------------------------------------------- /tests/utils/server/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/utils/server/playground.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from '@playwright/test'; 2 | 3 | type ScriptOptionsKeys = 'version' | 'server' | 'selector'; 4 | type ScriptOptions = { 5 | [key in ScriptOptionsKeys]?: string; 6 | }; 7 | function getOptions(options: ScriptOptions = {}) { 8 | return Object.entries(options) 9 | .filter(([, val]) => val) 10 | .map(([key, val]) => `data-${key}="${val}"`) 11 | .join(' '); 12 | } 13 | 14 | export async function gotoHtmlWidget(page: Page, html: string): Promise; 15 | export async function gotoHtmlWidget( 16 | page: Page, 17 | config: ScriptOptions, 18 | html: string, 19 | ): Promise; 20 | export async function gotoHtmlWidget( 21 | page: Page, 22 | config: ScriptOptions | string, 23 | html?: string, 24 | ): Promise { 25 | let content: string = html; 26 | let options: ScriptOptions = null; 27 | 28 | if (typeof config === 'string') { 29 | content = config; 30 | options = null; 31 | } else { 32 | options = config; 33 | } 34 | 35 | await page.goto('/blank.html'); 36 | 37 | await page.setContent(` 38 |
39 | 42 | 43 |
44 | `); 45 | 46 | const body = await page.evaluateHandle(() => document.body); 47 | await body.evaluate(() => document.forms[0].submit()); 48 | 49 | await expect(page.locator('[data-page="playground"]')).toHaveCount(1); 50 | 51 | return page; 52 | } 53 | -------------------------------------------------------------------------------- /tests/wasm-compatibility.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page, test } from '@playwright/test'; 2 | 3 | import { readFileSync } from 'fs'; 4 | import { join } from 'path'; 5 | 6 | import { gotoHtmlWidget } from './utils/server/playground'; 7 | import { RESULT_SELECTOR, WIDGET_SELECTOR } from './utils/selectors'; 8 | import { checkRunCase, prepareNetwork, printlnCode, toPostData } from './utils'; 9 | import { makeJSPrintCode } from './utils/mocks/wasm-1.9/result'; 10 | 11 | const WASM_1_9 = JSON.parse( 12 | readFileSync(join(__dirname, 'utils/mocks/wasm-1.9/wasm.json'), 'utf-8'), 13 | ); 14 | 15 | const JS_1_9 = Object.freeze({ 16 | jsCode: makeJSPrintCode('Hello, world!'), 17 | errors: { 'File.kt': [] }, 18 | exception: null, 19 | text: '', 20 | }); 21 | 22 | const OUTPUTS = Object.freeze({ 23 | 'js-ir': JS_1_9, 24 | wasm: WASM_1_9, 25 | }); 26 | 27 | test.describe('WASM platform with `moduleId` in output', () => { 28 | test.beforeEach(async ({ page, baseURL }) => { 29 | await prepareNetwork(page, baseURL); // offline mode 30 | }); 31 | 32 | test('JS 1.9 server response', async ({ page }) => { 33 | await run(page, 'js-ir'); 34 | }); 35 | 36 | test('WASM 1.9 server response', async ({ page }) => { 37 | await run(page, 'wasm'); 38 | }); 39 | }); 40 | 41 | async function run(page: Page, platform: keyof typeof OUTPUTS) { 42 | const version = '1.9.20'; 43 | const text = 'Hello, world!'; 44 | const source = printlnCode(text); 45 | await gotoHtmlWidget( 46 | page, 47 | { selector: 'code', version }, 48 | `${source}`, 49 | ); 50 | const editor = page.locator(WIDGET_SELECTOR); 51 | const postData = `{"args":"","files":[{"name":"File.kt","text":"${toPostData(source)}","publicId":""}],"confType":"${platform}"}`; 52 | await checkRunCase(page, editor, postData, { json: OUTPUTS[platform] }); 53 | await expect(editor.locator(RESULT_SELECTOR)).toHaveText(text); 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /utils/copy-examples.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { cp, sed } = require('shelljs'); 3 | 4 | const projectDir = path.resolve(__dirname, '..'); 5 | 6 | cp('-R', `${projectDir}/dist/examples`, projectDir); 7 | 8 | sed( 9 | '-i', 10 | '../playground.js', 11 | 'https://unpkg.com/kotlin-playground@1', 12 | `${projectDir}/examples/index.html` 13 | ); 14 | -------------------------------------------------------------------------------- /utils/markdown-loader.js: -------------------------------------------------------------------------------- 1 | const MarkdownIt = require('markdown-it'); 2 | const highlightPreset = require('markdown-it-highlightjs'); 3 | 4 | const md = new MarkdownIt({ 5 | html: true 6 | }); 7 | 8 | md.use(highlightPreset); 9 | 10 | module.exports = function(source) { 11 | const done = this.async(); 12 | const result = md.render(source); 13 | 14 | done(null, `module.exports = ${JSON.stringify(result)}`); 15 | }; 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = (params = {}) => { 6 | const isProduction = params.production; 7 | const env = isProduction ? 'production' : 'development'; 8 | const mainEntryName = isProduction ? 'playground.min' : 'playground'; 9 | const isServer = process.argv[1].includes('webpack-dev-server'); 10 | const libraryName = 'KotlinPlayground'; 11 | const webDemoUrl = params.webDemoUrl || 'https://api.kotlinlang.org/'; 12 | const webDemoResourcesUrl = 13 | params.webDemoResourcesUrl || 'https://api.kotlinlang.org/'; 14 | const examplesPath = isServer ? '' : 'examples/'; 15 | const pathDist = path.resolve(__dirname, 'dist'); 16 | 17 | const common = { 18 | mode: env, 19 | 20 | output: { 21 | path: pathDist, 22 | filename: '[name].js', 23 | }, 24 | 25 | devtool: 'source-map', 26 | 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.js$/, 31 | include: path.resolve(__dirname, 'src'), 32 | loader: 'babel-loader', 33 | }, 34 | { 35 | test: /\.tsx?$/, 36 | use: 'ts-loader', 37 | exclude: /node_modules/, 38 | }, 39 | { 40 | test: /\.monk$/, 41 | loader: 'monkberry-loader', 42 | }, 43 | { 44 | test: /\.s[ac]ss$/i, 45 | use: ['style-loader', 'css-loader', 'sass-loader'], 46 | }, 47 | { 48 | test: /\.svg$/, 49 | use: ['svg-url-loader', 'svg-fill-loader'], 50 | type: 'javascript/auto', 51 | }, 52 | { 53 | test: /\.md$/, 54 | loader: path.resolve(__dirname, 'utils/markdown-loader.js'), 55 | }, 56 | ], 57 | }, 58 | 59 | resolve: { 60 | extensions: ['.tsx', '.ts', '.js'], 61 | }, 62 | 63 | plugins: [ 64 | new webpack.optimize.ModuleConcatenationPlugin(), 65 | new webpack.DefinePlugin({ 66 | 'process.env': { 67 | NODE_ENV: JSON.stringify(env), 68 | }, 69 | }), 70 | ], 71 | }; 72 | 73 | const bundle = { 74 | ...common, 75 | 76 | entry: { 77 | [mainEntryName]: ['./src/index'], 78 | REMOVE_ME: [ 79 | `!!file-loader?name=${examplesPath}examples.css!github-markdown-css/github-markdown.css`, 80 | `!!file-loader?name=${examplesPath}examples-highlight.css!highlight.js/styles/github.css`, 81 | ], 82 | }, 83 | 84 | output: { 85 | ...common.output, 86 | library: libraryName, 87 | libraryTarget: 'umd', 88 | libraryExport: 'default', 89 | publicPath: 'auto', 90 | }, 91 | 92 | plugins: [ 93 | ...common.plugins, 94 | 95 | new HtmlPlugin({ 96 | template: 'examples.md', 97 | filename: isServer ? 'index.html' : 'examples/index.html', 98 | inject: false, 99 | }), 100 | 101 | new webpack.DefinePlugin({ 102 | __WEBDEMO_URL__: JSON.stringify(webDemoUrl), 103 | __WEBDEMO_RESOURCES_URL__: JSON.stringify(webDemoResourcesUrl), 104 | __IS_PRODUCTION__: isProduction, 105 | __LIBRARY_NAME__: JSON.stringify(libraryName), 106 | }), 107 | ], 108 | devServer: { 109 | static: path.resolve(__dirname, 'src'), 110 | headers: { 111 | 'Access-Control-Allow-Origin': '*', 112 | 'Access-Control-Allow-Methods': 113 | 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 114 | 'Access-Control-Allow-Headers': 115 | 'X-Requested-With, content-type, Authorization', 116 | }, 117 | }, 118 | }; 119 | 120 | const crosslink = { 121 | ...common, 122 | target: 'node', 123 | entry: { 124 | crosslink: './src/lib/crosslink', 125 | }, 126 | output: { 127 | ...common.output, 128 | globalObject: 'this', 129 | library: { 130 | name: 'crosslink', 131 | type: 'umd', 132 | }, 133 | }, 134 | }; 135 | 136 | return [bundle, crosslink]; 137 | }; 138 | --------------------------------------------------------------------------------