├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── default.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── ts-gyb ├── demo ├── basic │ ├── config.json │ ├── generated │ │ ├── kotlin │ │ │ ├── BridgeTypes.kt │ │ │ ├── IHtmlApi.kt │ │ │ └── IImageOptionApi.kt │ │ └── swift │ │ │ ├── IHtmlApi.swift │ │ │ ├── IImageOptionApi.swift │ │ │ └── SharedTypes.swift │ └── interfaces.ts └── mini-editor │ ├── android │ ├── .gitignore │ ├── .idea │ │ ├── .gitignore │ │ ├── .name │ │ ├── compiler.xml │ │ ├── gradle.xml │ │ ├── jarRepositories.xml │ │ ├── misc.xml │ │ ├── runConfigurations.xml │ │ └── vcs.xml │ ├── app │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── androidTest │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── microsoft │ │ │ │ └── tscodegen │ │ │ │ └── demo │ │ │ │ └── minieditor │ │ │ │ └── ExampleInstrumentedTest.kt │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── microsoft │ │ │ │ │ └── tscodegen │ │ │ │ │ └── demo │ │ │ │ │ └── minieditor │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── generated │ │ │ │ │ └── EditorBridge.kt │ │ │ └── res │ │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ ├── drawable │ │ │ │ └── ic_launcher_background.xml │ │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ │ └── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ └── test │ │ │ └── java │ │ │ └── com │ │ │ └── microsoft │ │ │ └── tscodegen │ │ │ └── demo │ │ │ └── minieditor │ │ │ └── ExampleUnitTest.kt │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle │ ├── apple │ ├── .gitignore │ ├── MiniEditor.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── MiniEditor.xcscheme │ └── MiniEditor │ │ ├── ActionButtion.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ │ ├── Generated │ │ └── EditorBridge.swift │ │ ├── Info.plist │ │ ├── MiniEditor.entitlements │ │ ├── Resources │ │ └── .gitkeep │ │ ├── SceneDelegate.swift │ │ ├── Toolbar.swift │ │ ├── ViewController.swift │ │ └── WebView.swift │ └── web │ ├── code-templates │ ├── kotlin-named-type.mustache │ ├── kotlin-named-types.mustache │ ├── kotlin.mustache │ ├── swift-named-type.mustache │ └── swift.mustache │ ├── config.json │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── editor │ │ ├── Editor.ts │ │ └── IEditor.ts │ ├── index.html │ ├── main.ts │ ├── types │ │ └── types.d.ts │ └── utils │ │ └── onReady.ts │ ├── tsconfig-for-webpack-config.json │ ├── tsconfig.json │ └── webpack.config.ts ├── documentation ├── generated │ └── interfaces │ │ ├── Configuration.md │ │ ├── LanguageRenderingConfiguration.md │ │ ├── ParseConfiguration.md │ │ └── RenderConfiguration.md ├── interface-guide.md ├── predefined-type.md └── template-guide.md ├── example-templates ├── kotlin-bridge.mustache ├── kotlin-named-type.mustache ├── kotlin-named-types.mustache ├── swift-bridge.mustache ├── swift-named-type.mustache ├── swift-named-types.mustache └── swift-native-module.mustache ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── cli │ ├── generate.ts │ └── index.ts ├── configuration.ts ├── generator │ ├── CodeGenerator.ts │ ├── named-types.ts │ └── utils.ts ├── index.ts ├── logger │ └── ParserLogger.ts ├── parser │ ├── Parser.ts │ ├── ValueParser.ts │ ├── ValueParserError.ts │ └── utils.ts ├── renderer │ ├── renderer.ts │ ├── utils.ts │ ├── value-transformer │ │ ├── KotlinValueTransformer.ts │ │ ├── SwiftValueTransformer.ts │ │ ├── ValueTransformer.ts │ │ └── index.ts │ └── views │ │ ├── EnumTypeView.ts │ │ ├── InterfaceTypeView.ts │ │ ├── MethodView.ts │ │ ├── ModuleView.ts │ │ ├── UnionTypeView.ts │ │ └── index.ts ├── serializers.ts ├── types.ts └── utils.ts ├── test ├── parser-test.ts ├── utils.ts └── value-parser-test.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | root: true, 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { 6 | "project": "tsconfig.json", 7 | "sourceType": "module" 8 | }, 9 | env: { 10 | "browser": true, 11 | }, 12 | plugins: [ 13 | "@typescript-eslint", 14 | ], 15 | extends: [ 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 18 | "eslint-config-airbnb-base", 19 | "eslint-config-prettier" 20 | ], 21 | settings: { 22 | "import/resolver": { 23 | "node": { 24 | "extensions": [".ts"] 25 | } 26 | } 27 | }, 28 | rules: { 29 | "import/no-unresolved": [2], 30 | "@typescript-eslint/explicit-function-return-type": "error", 31 | "@typescript-eslint/explicit-module-boundary-types": "error", 32 | "@typescript-eslint/prefer-regexp-exec": "warn", 33 | "@typescript-eslint/restrict-template-expressions": "warn", 34 | "@typescript-eslint/no-unsafe-member-access": "warn", 35 | "@typescript-eslint/no-unnecessary-type-assertion": "warn", 36 | "@typescript-eslint/no-unsafe-return": "warn", 37 | "@typescript-eslint/no-unsafe-call": "warn", 38 | "@typescript-eslint/no-unsafe-assignment": "warn", 39 | "@typescript-eslint/no-empty-interface": "off", 40 | "@typescript-eslint/no-namespace": "off", 41 | "@typescript-eslint/semi": [ 42 | "error", 43 | "always" 44 | ], 45 | "@typescript-eslint/no-unused-vars": "error", 46 | "no-shadow": "off", 47 | "@typescript-eslint/no-shadow": ["error"], 48 | "no-use-before-define": "off", 49 | "@typescript-eslint/no-use-before-define": [ 50 | "error", 51 | { "functions": false } 52 | ], 53 | "no-unused-vars": "off", 54 | "no-useless-escape": "warn", 55 | "no-prototype-builtins": "warn", 56 | "no-console": "off", 57 | "arrow-parens": [ 58 | "off", 59 | "always" 60 | ], 61 | "sort-keys": "off", 62 | "max-len": "off", 63 | "no-bitwise": "off", 64 | "no-duplicate-case": "error", 65 | "quotes": ["off", "single"], 66 | "curly": "error", 67 | "import/extensions": "off", 68 | "class-methods-use-this": "off", 69 | "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }], 70 | "camelcase": ["error", {"allow": ["UNSAFE_componentWillReceiveProps"]}], 71 | "import/prefer-default-export": "off", 72 | "import/no-default-export": "error", 73 | "no-param-reassign": ["error", { "props": false }], 74 | "no-underscore-dangle": ["error", { "enforceInMethodNames": true, "allowAfterThis": true }], 75 | "no-useless-constructor": "off", 76 | "no-empty-function": ["error", {"allow": ["constructors"]}], 77 | "object-curly-spacing": "error", 78 | }, 79 | "overrides": [ 80 | { 81 | "files": ["test/**/*.ts"], 82 | "rules": { 83 | "no-unused-expressions": "off" 84 | } 85 | } 86 | ], 87 | }; 88 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install dependencies 18 | run: npm install 19 | 20 | - name: Build 21 | run: | 22 | npm run build 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Test 33 | run: | 34 | npm run test 35 | 36 | lint: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - name: Install dependencies 42 | run: npm install 43 | 44 | - name: Lint 45 | run: | 46 | echo Linking basic-types into ./src 47 | ln -s $(pwd)/basic-types ./src/ 48 | npm run lint 49 | 50 | demo-codegen: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v2 54 | 55 | - name: Install dependencies 56 | run: npm install 57 | 58 | - name: Code generation for demo projects 59 | run: | 60 | npm run build 61 | npm run start:example:mini-editor 62 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Pipeline 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | npm-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.event.release.target_commitish }} 14 | - name: Use Node.js 14 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 14 18 | - name: match-tag-to-package-version 19 | uses: geritol/match-tag-to-package-version@0.0.2 20 | env: 21 | TAG_PREFIX: refs/tags/v 22 | - run: npm install 23 | - run: npm run build 24 | - run: npm config set '//registry.npmjs.org/:_authToken' "${{ secrets.NPM_TOKEN }}" 25 | - run: npm publish 26 | env: 27 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | basic-types/dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # VSCode debug config 113 | .vscode/launch.json 114 | 115 | # yarn v2 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .yarn/install-state.gz 120 | .pnp.* 121 | 122 | # publish config 123 | .npmrc 124 | 125 | # debug config 126 | **/.vscode/launch.json 127 | 128 | src/basic-types 129 | 130 | .DS_Store 131 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /bin/ts-gyb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist/cli/index.js') 3 | -------------------------------------------------------------------------------- /demo/basic/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "parsing": { 3 | "targets": { 4 | "api": { 5 | "source": ["interfaces.ts"] 6 | } 7 | }, 8 | "predefinedTypes": [ 9 | "CodeGen_Int" 10 | ], 11 | "defaultCustomTags": {}, 12 | "dropInterfaceIPrefix": true 13 | }, 14 | "rendering": { 15 | "swift": { 16 | "renders": [ 17 | { 18 | "target": "api", 19 | "template": "../../example-templates/swift-bridge.mustache", 20 | "outputPath": "generated/swift" 21 | } 22 | ], 23 | "namedTypesTemplatePath": "../../example-templates/swift-named-types.mustache", 24 | "namedTypesOutputPath": "generated/swift/SharedTypes.swift", 25 | "typeNameMap": { 26 | "CodeGen_Int": "Int" 27 | } 28 | }, 29 | "kotlin": { 30 | "renders": [ 31 | { 32 | "target": "api", 33 | "template": "../../example-templates/kotlin-bridge.mustache", 34 | "outputPath": "generated/kotlin" 35 | } 36 | ], 37 | "namedTypesTemplatePath": "../../example-templates/kotlin-named-types.mustache", 38 | "namedTypesOutputPath": "generated/kotlin/BridgeTypes.kt", 39 | "typeNameMap": { 40 | "CodeGen_Int": "Int", 41 | "BaseSize": "JSBaseSize" 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo/basic/generated/kotlin/IHtmlApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. 3 | * Microsoft Corporation. All rights reserved. 4 | * 5 | * 6 | * This file is automatically generated 7 | * Please DO NOT modify 8 | */ 9 | 10 | package com.microsoft.office.outlook.rooster.web.bridge 11 | 12 | import com.google.gson.Gson 13 | import com.microsoft.office.outlook.rooster.Callback 14 | import com.microsoft.office.outlook.rooster.web.JsBridge 15 | import com.microsoft.office.outlook.rooster.web.WebEditor 16 | import java.lang.reflect.Type 17 | import com.google.gson.JsonDeserializationContext 18 | import com.google.gson.JsonDeserializer 19 | import com.google.gson.JsonElement 20 | import com.google.gson.JsonPrimitive 21 | import com.google.gson.JsonSerializationContext 22 | import com.google.gson.JsonSerializer 23 | import com.google.gson.annotations.SerializedName 24 | 25 | interface IHtmlApiBridge { 26 | fun setMentionClassNames(idToClassNames: Map>) 27 | fun getHeight(callback: Callback) 28 | fun getHeightWithBottomAnchor(sta: Array, callback: Callback) 29 | fun getHTML(title: String, callback: Callback) 30 | fun requestRenderingResult() 31 | fun getSize(callback: Callback) 32 | fun getAliasSize(callback: Callback) 33 | fun getName(callback: Callback) 34 | fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) 35 | fun testDictionaryWithAnyKey(dict: Map) 36 | fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello", callback: Callback) 37 | } 38 | 39 | open class IHtmlApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "htmlApi"), IHtmlApiBridge { 40 | 41 | override fun setMentionClassNames(idToClassNames: Map>) { 42 | executeJs("setMentionClassNames", mapOf( 43 | "idToClassNames" to idToClassNames 44 | )) 45 | } 46 | 47 | override fun getHeight(callback: Callback) { 48 | executeJsForResponse(Float::class.java, "getHeight", callback) 49 | } 50 | 51 | override fun getHeightWithBottomAnchor(sta: Array, callback: Callback) { 52 | executeJsForResponse(Float::class.java, "getHeightWithBottomAnchor", callback, mapOf( 53 | "sta" to sta 54 | )) 55 | } 56 | 57 | override fun getHTML(title: String, callback: Callback) { 58 | executeJsForResponse(String::class.java, "getHTML", callback, mapOf( 59 | "title" to title 60 | )) 61 | } 62 | 63 | override fun requestRenderingResult() { 64 | executeJs("requestRenderingResult") 65 | } 66 | 67 | override fun getSize(callback: Callback) { 68 | executeJsForResponse(OverriddenFullSize::class.java, "getSize", callback) 69 | } 70 | 71 | override fun getAliasSize(callback: Callback) { 72 | executeJsForResponse(JSBaseSize::class.java, "getAliasSize", callback) 73 | } 74 | 75 | override fun getName(callback: Callback) { 76 | executeJsForResponse(IHtmlApiGetNameReturnType::class.java, "getName", callback) 77 | } 78 | 79 | override fun getAge(gender: IHtmlApiGetAgeGender, callback: Callback) { 80 | executeJsForResponse(IHtmlApiGetAgeReturnType::class.java, "getAge", callback, mapOf( 81 | "gender" to gender 82 | )) 83 | } 84 | 85 | override fun testDictionaryWithAnyKey(dict: Map) { 86 | executeJs("testDictionaryWithAnyKey", mapOf( 87 | "dict" to dict 88 | )) 89 | } 90 | 91 | override fun testDefaultValue(bool: Boolean? = null, bool2: Boolean?, bool3: Boolean = true, num: Float = 1, string: String = "hello", callback: Callback) { 92 | executeJsForResponse(ObjectWithDefeaultValue::class.java, "testDefaultValue", callback, mapOf( 93 | "bool" to bool 94 | "bool2" to bool2 95 | "bool3" to bool3 96 | "num" to num 97 | "string" to string 98 | )) 99 | } 100 | } 101 | 102 | data class JSBaseSize( 103 | @JvmField val width: Float, 104 | @JvmField val height: Float, 105 | ) 106 | 107 | enum class IHtmlApiGetNameReturnType { 108 | @SerializedName("A2") A2, 109 | @SerializedName("B2") B2 110 | } 111 | 112 | enum class IHtmlApiGetAgeGender { 113 | @SerializedName("Male") MALE, 114 | @SerializedName("Female") FEMALE 115 | } 116 | 117 | enum class IHtmlApiGetAgeReturnType(val value: Int) { 118 | _21(21), 119 | _22(22); 120 | 121 | companion object { 122 | fun find(value: Int) = values().find { it.value == value } 123 | } 124 | } 125 | 126 | class IHtmlApiGetAgeReturnTypeTypeAdapter : JsonSerializer, JsonDeserializer { 127 | override fun serialize(obj: IHtmlApiGetAgeReturnType, type: Type, context: JsonSerializationContext): JsonElement { 128 | return JsonPrimitive(obj.value) 129 | } 130 | 131 | override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): IHtmlApiGetAgeReturnType? { 132 | return IHtmlApiGetAgeReturnType.find(json.asInt) 133 | } 134 | } 135 | 136 | data class ObjectWithDefeaultValue( 137 | @JvmField val defaultValue: Boolean? = true, 138 | ) 139 | -------------------------------------------------------------------------------- /demo/basic/generated/kotlin/IImageOptionApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. 3 | * Microsoft Corporation. All rights reserved. 4 | * 5 | * 6 | * This file is automatically generated 7 | * Please DO NOT modify 8 | */ 9 | 10 | package com.microsoft.office.outlook.rooster.web.bridge 11 | 12 | import com.google.gson.Gson 13 | import com.microsoft.office.outlook.rooster.Callback 14 | import com.microsoft.office.outlook.rooster.web.JsBridge 15 | import com.microsoft.office.outlook.rooster.web.WebEditor 16 | 17 | interface IImageOptionApiBridge { 18 | fun hideElementWithID(id: String) 19 | fun restoreElementVisibilityWithID(id: String) 20 | fun getSourceOfImageWithID(id: String, callback: Callback) 21 | fun getImageDataList(callback: Callback) 22 | fun getContentBoundsOfElementWithID(id: String, callback: Callback) 23 | fun getSize(callback: Callback) 24 | } 25 | 26 | open class IImageOptionApiBridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "imageOption"), IImageOptionApiBridge { 27 | 28 | override fun hideElementWithID(id: String) { 29 | executeJs("hideElementWithID", mapOf( 30 | "id" to id 31 | )) 32 | } 33 | 34 | override fun restoreElementVisibilityWithID(id: String) { 35 | executeJs("restoreElementVisibilityWithID", mapOf( 36 | "id" to id 37 | )) 38 | } 39 | 40 | override fun getSourceOfImageWithID(id: String, callback: Callback) { 41 | executeJsForResponse(String::class.java, "getSourceOfImageWithID", callback, mapOf( 42 | "id" to id 43 | )) 44 | } 45 | 46 | override fun getImageDataList(callback: Callback) { 47 | executeJsForResponse(String::class.java, "getImageDataList", callback) 48 | } 49 | 50 | override fun getContentBoundsOfElementWithID(id: String, callback: Callback) { 51 | executeJsForResponse(String::class.java, "getContentBoundsOfElementWithID", callback, mapOf( 52 | "id" to id 53 | )) 54 | } 55 | 56 | override fun getSize(callback: Callback) { 57 | executeJsForResponse(OverriddenFullSize::class.java, "getSize", callback) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /demo/basic/generated/swift/IHtmlApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | /// Documentation for module 9 | public class IHtmlApi { 10 | let jsExecutor: BridgeJSExecutor 11 | 12 | init(jsExecutor: BridgeJSExecutor) { 13 | self.jsExecutor = jsExecutor 14 | } 15 | 16 | /// This is a documentation 17 | /// Set Mention class names 18 | /// used to map id to class names 19 | public func setMentionClassNames(idToClassNames: [String: [String]], completion: BridgeJSExecutor.Completion? = nil) { 20 | struct Args: Encodable { 21 | let idToClassNames: [String: [String]] 22 | } 23 | let args = Args( 24 | idToClassNames: idToClassNames 25 | ) 26 | jsExecutor.execute(with: "htmlApi", feature: "setMentionClassNames", args: args, completion: completion) 27 | } 28 | 29 | public func getHeight(completion: @escaping BridgeCompletion) { 30 | jsExecutor.execute(with: "htmlApi", feature: "getHeight", args: nil, completion: completion) 31 | } 32 | 33 | public func getHeightWithBottomAnchor(sta: [String], completion: @escaping BridgeCompletion) { 34 | struct Args: Encodable { 35 | let sta: [String] 36 | } 37 | let args = Args( 38 | sta: sta 39 | ) 40 | jsExecutor.execute(with: "htmlApi", feature: "getHeightWithBottomAnchor", args: args, completion: completion) 41 | } 42 | 43 | public func getHTML(title: String, completion: @escaping BridgeCompletion) { 44 | struct Args: Encodable { 45 | let title: String 46 | } 47 | let args = Args( 48 | title: title 49 | ) 50 | jsExecutor.execute(with: "htmlApi", feature: "getHTML", args: args, completion: completion) 51 | } 52 | 53 | public func requestRenderingResult(completion: BridgeJSExecutor.Completion? = nil) { 54 | jsExecutor.execute(with: "htmlApi", feature: "requestRenderingResult", args: nil, completion: completion) 55 | } 56 | 57 | public func getSize(completion: @escaping BridgeCompletion) { 58 | jsExecutor.execute(with: "htmlApi", feature: "getSize", args: nil, completion: completion) 59 | } 60 | 61 | public func getAliasSize(completion: @escaping BridgeCompletion) { 62 | jsExecutor.execute(with: "htmlApi", feature: "getAliasSize", args: nil, completion: completion) 63 | } 64 | 65 | public func getName(completion: @escaping BridgeCompletion) { 66 | jsExecutor.execute(with: "htmlApi", feature: "getName", args: nil, completion: completion) 67 | } 68 | 69 | public func getAge(gender: IHtmlApiGetAgeGender, completion: @escaping BridgeCompletion) { 70 | struct Args: Encodable { 71 | let gender: IHtmlApiGetAgeGender 72 | } 73 | let args = Args( 74 | gender: gender 75 | ) 76 | jsExecutor.execute(with: "htmlApi", feature: "getAge", args: args, completion: completion) 77 | } 78 | 79 | public func testDictionaryWithAnyKey(dict: [String: String], completion: BridgeJSExecutor.Completion? = nil) { 80 | struct Args: Encodable { 81 | let dict: [String: String] 82 | } 83 | let args = Args( 84 | dict: dict 85 | ) 86 | jsExecutor.execute(with: "htmlApi", feature: "testDictionaryWithAnyKey", args: args, completion: completion) 87 | } 88 | 89 | public func testDefaultValue(bool: Bool? = nil, bool2: Bool?, bool3: Bool = true, num: Double = 1, string: String = "hello", completion: @escaping BridgeCompletion) { 90 | struct Args: Encodable { 91 | let bool: Bool? 92 | let bool2: Bool? 93 | let bool3: Bool 94 | let num: Double 95 | let string: String 96 | } 97 | let args = Args( 98 | bool: bool, 99 | bool2: bool2, 100 | bool3: bool3, 101 | num: num, 102 | string: string 103 | ) 104 | jsExecutor.execute(with: "htmlApi", feature: "testDefaultValue", args: args, completion: completion) 105 | } 106 | } 107 | 108 | public struct BaseSize: Codable { 109 | public var width: Double 110 | public var height: Double 111 | 112 | public init(width: Double, height: Double) { 113 | self.width = width 114 | self.height = height 115 | } 116 | } 117 | 118 | public enum IHtmlApiGetNameReturnType: String, Codable { 119 | case a2 = "A2" 120 | case b2 = "B2" 121 | } 122 | 123 | public enum IHtmlApiGetAgeGender: String, Codable { 124 | case male = "Male" 125 | case female = "Female" 126 | } 127 | 128 | public enum IHtmlApiGetAgeReturnType: Int, Codable { 129 | case _21 = 21 130 | case _22 = 22 131 | } 132 | 133 | public struct ObjectWithDefeaultValue: Codable { 134 | public var defaultValue: Bool? 135 | 136 | public init(defaultValue: Bool? = true) { 137 | self.defaultValue = defaultValue 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /demo/basic/generated/swift/IImageOptionApi.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | public class IImageOptionApi { 9 | let jsExecutor: BridgeJSExecutor 10 | 11 | init(jsExecutor: BridgeJSExecutor) { 12 | self.jsExecutor = jsExecutor 13 | } 14 | 15 | public func hideElementWithID(id: String, completion: BridgeJSExecutor.Completion? = nil) { 16 | struct Args: Encodable { 17 | let id: String 18 | } 19 | let args = Args( 20 | id: id 21 | ) 22 | jsExecutor.execute(with: "imageOption", feature: "hideElementWithID", args: args, completion: completion) 23 | } 24 | 25 | public func restoreElementVisibilityWithID(id: String, completion: BridgeJSExecutor.Completion? = nil) { 26 | struct Args: Encodable { 27 | let id: String 28 | } 29 | let args = Args( 30 | id: id 31 | ) 32 | jsExecutor.execute(with: "imageOption", feature: "restoreElementVisibilityWithID", args: args, completion: completion) 33 | } 34 | 35 | public func getSourceOfImageWithID(id: String, completion: @escaping BridgeCompletion) { 36 | struct Args: Encodable { 37 | let id: String 38 | } 39 | let args = Args( 40 | id: id 41 | ) 42 | jsExecutor.execute(with: "imageOption", feature: "getSourceOfImageWithID", args: args, completion: completion) 43 | } 44 | 45 | public func getImageDataList(completion: @escaping BridgeCompletion) { 46 | jsExecutor.execute(with: "imageOption", feature: "getImageDataList", args: nil, completion: completion) 47 | } 48 | 49 | public func getContentBoundsOfElementWithID(id: String, completion: @escaping BridgeCompletion) { 50 | struct Args: Encodable { 51 | let id: String 52 | } 53 | let args = Args( 54 | id: id 55 | ) 56 | jsExecutor.execute(with: "imageOption", feature: "getContentBoundsOfElementWithID", args: args, completion: completion) 57 | } 58 | 59 | public func getSize(completion: @escaping BridgeCompletion) { 60 | jsExecutor.execute(with: "imageOption", feature: "getSize", args: nil, completion: completion) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /demo/basic/generated/swift/SharedTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | import UIKit 9 | 10 | /// Example documentation for interface 11 | public struct OverriddenFullSize: Codable { 12 | public var size: Double 13 | public var count: Int 14 | public var stringEnum: StringEnum 15 | public var numEnum: NumEnum 16 | public var defEnum: DefaultEnum 17 | public var stringUnion: OverriddenFullSizeMembersStringUnionType 18 | public var numberStringUnion: OverriddenFullSizeMembersNumberStringUnionType 19 | public var nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType? 20 | public var numUnion1: OverriddenFullSizeMembersNumUnion1Type 21 | public var foo: OverriddenFullSizeMembersFooType 22 | public var unionType: OverriddenFullSizeMembersUnionTypeType 23 | public var width: Double 24 | public var height: Double 25 | public var scale: Double 26 | /// Example documentation for member 27 | private var member: NumEnum = .one 28 | 29 | public init(size: Double, count: Int, stringEnum: StringEnum, numEnum: NumEnum, defEnum: DefaultEnum, stringUnion: OverriddenFullSizeMembersStringUnionType, numberStringUnion: OverriddenFullSizeMembersNumberStringUnionType, nullableStringUnion: OverriddenFullSizeMembersNullableStringUnionType?, numUnion1: OverriddenFullSizeMembersNumUnion1Type, foo: OverriddenFullSizeMembersFooType, unionType: OverriddenFullSizeMembersUnionTypeType, width: Double, height: Double, scale: Double) { 30 | self.size = size 31 | self.count = count 32 | self.stringEnum = stringEnum 33 | self.numEnum = numEnum 34 | self.defEnum = defEnum 35 | self.stringUnion = stringUnion 36 | self.numberStringUnion = numberStringUnion 37 | self.nullableStringUnion = nullableStringUnion 38 | self.numUnion1 = numUnion1 39 | self.foo = foo 40 | self.unionType = unionType 41 | self.width = width 42 | self.height = height 43 | self.scale = scale 44 | } 45 | } 46 | 47 | public enum NumEnum: Int, Codable { 48 | case `default` = 0 49 | case one = 1 50 | case two = 2 51 | } 52 | 53 | public enum StringEnum: String, Codable { 54 | case `default` = "default" 55 | /// Description for enum member a 56 | case a = "a" 57 | case b = "b" 58 | } 59 | 60 | public enum DefaultEnum: Int, Codable { 61 | case `default` = 0 62 | case defaultValueC = 1 63 | case defaultValueD = 2 64 | } 65 | 66 | public enum OverriddenFullSizeMembersStringUnionType: String, Codable { 67 | case a1 = "A1" 68 | case b1 = "B1" 69 | } 70 | 71 | public enum OverriddenFullSizeMembersNumberStringUnionType: String, Codable { 72 | case _11 = "11" 73 | case _21 = "21" 74 | } 75 | 76 | public enum OverriddenFullSizeMembersNullableStringUnionType: String, Codable { 77 | case a1 = "A1" 78 | case b1 = "B1" 79 | } 80 | 81 | public enum OverriddenFullSizeMembersNumUnion1Type: Int, Codable { 82 | case _11 = 11 83 | case _21 = 21 84 | } 85 | 86 | public struct OverriddenFullSizeMembersFooType: Codable { 87 | public var stringField: String 88 | public var numberField: Double 89 | 90 | public init(stringField: String, numberField: Double) { 91 | self.stringField = stringField 92 | self.numberField = numberField 93 | } 94 | } 95 | 96 | public enum OverriddenFullSizeMembersUnionTypeType: Codable { 97 | case numEnum(_ value: NumEnum) 98 | case defaultEnum(_ value: DefaultEnum) 99 | case stringArray(_ value: [String]) 100 | case stringForStringDictionary(_ value: [String: String]) 101 | case bool(_ value: Bool) 102 | case double(_ value: Double) 103 | case string(_ value: String) 104 | 105 | public init(from decoder: any Decoder) throws { 106 | let container = try decoder.singleValueContainer() 107 | if let value = try? container.decode(NumEnum.self) { 108 | self = .numEnum(value) 109 | } 110 | else if let value = try? container.decode(DefaultEnum.self) { 111 | self = .defaultEnum(value) 112 | } 113 | else if let value = try? container.decode([String].self) { 114 | self = .stringArray(value) 115 | } 116 | else if let value = try? container.decode([String: String].self) { 117 | self = .stringForStringDictionary(value) 118 | } 119 | else if let value = try? container.decode(Bool.self) { 120 | self = .bool(value) 121 | } 122 | else if let value = try? container.decode(Double.self) { 123 | self = .double(value) 124 | } 125 | else { 126 | let value = try container.decode(String.self) 127 | self = .string(value) 128 | } 129 | } 130 | 131 | public func encode(to encoder: any Encoder) throws { 132 | var container = encoder.singleValueContainer() 133 | switch self { 134 | case .numEnum(let value): 135 | try container.encode(value) 136 | case .defaultEnum(let value): 137 | try container.encode(value) 138 | case .stringArray(let value): 139 | try container.encode(value) 140 | case .stringForStringDictionary(let value): 141 | try container.encode(value) 142 | case .bool(let value): 143 | try container.encode(value) 144 | case .double(let value): 145 | try container.encode(value) 146 | case .string(let value): 147 | try container.encode(value) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /demo/basic/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | type CodeGen_Int = number & { _intBrand: never }; 4 | type str = string; 5 | type AliasSize = BaseSize; 6 | interface BaseSize { 7 | width: number; 8 | height: number; 9 | } 10 | 11 | interface CustomSize { 12 | scale: number; 13 | } 14 | 15 | enum StringEnum { 16 | default = 'default', 17 | /** 18 | * Description for enum member a 19 | */ 20 | a = 'a', 21 | b = 'b', 22 | } 23 | 24 | enum NumEnum { 25 | default = 0, 26 | one = 1, 27 | two = 2, 28 | } 29 | 30 | enum DefaultEnum { 31 | default, 32 | defaultValueC, 33 | defaultValueD, 34 | } 35 | 36 | /** 37 | * Example documentation for interface 38 | * @overrideTypeName OverriddenFullSize 39 | */ 40 | interface FullSize extends BaseSize, CustomSize { 41 | /** 42 | * Example documentation for member 43 | */ 44 | member: NumEnum.one; 45 | size: number; 46 | count: CodeGen_Int; 47 | stringEnum: StringEnum; 48 | numEnum: NumEnum; 49 | defEnum: DefaultEnum; 50 | stringUnion: 'A1' | 'B1'; 51 | numberStringUnion: '11' | '21'; 52 | nullableStringUnion: 'A1' | 'B1' | null; 53 | numUnion1: 11 | 21; 54 | foo: { stringField: string } | { numberField: number }; 55 | unionType: string | number | boolean | NumEnum | DefaultEnum | string[] | DictionaryWithAnyKey; 56 | } 57 | 58 | interface DictionaryWithAnyKey { 59 | [key: string]: string; 60 | } 61 | 62 | 63 | interface ObjectWithDefeaultValue { 64 | /** 65 | * @default true 66 | */ 67 | defaultValue?: boolean; 68 | } 69 | 70 | /** 71 | * Documentation for module 72 | * @shouldExport true 73 | * @invokePath htmlApi 74 | */ 75 | export interface IHtmlApi { 76 | /** 77 | * This is a documentation 78 | * Set Mention class names 79 | * used to map id to class names 80 | */ 81 | setMentionClassNames({ idToClassNames }: { idToClassNames: { [id: string]: string[] } }): void; 82 | getHeight(): number; 83 | getHeightWithBottomAnchor({ sta }: { sta: string[] }): number; 84 | getHTML({ title }: { title: string }): str; 85 | requestRenderingResult(): void; 86 | getSize(): FullSize; 87 | getAliasSize(): AliasSize; 88 | getName(): 'A2' | 'B2'; 89 | getAge({ gender }: { gender: 'Male' | 'Female' }): 21 | 22; 90 | testDictionaryWithAnyKey({ dict }: { dict: DictionaryWithAnyKey }): void; 91 | 92 | testDefaultValue(options: { 93 | /** 94 | * @default null 95 | */ 96 | bool?: boolean; 97 | bool2?: boolean; 98 | /** 99 | * @default true 100 | */ 101 | bool3: boolean; 102 | /** 103 | * @default 1 104 | */ 105 | num: number; 106 | /** 107 | * @default "hello" 108 | */ 109 | string: string; 110 | }): ObjectWithDefeaultValue; 111 | } 112 | 113 | /** 114 | * @shouldExport true 115 | * @invokePath imageOption 116 | */ 117 | export interface IImageOptionApi { 118 | hideElementWithID({ id }: { id: string }): void; 119 | restoreElementVisibilityWithID({ id }: { id: string }): void; 120 | getSourceOfImageWithID({ id }: { id: string }): string | null; 121 | getImageDataList(): string; 122 | getContentBoundsOfElementWithID({ id }: { id: string }): string | null; 123 | getSize(): FullSize; 124 | } 125 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | 17 | # JavaScript bundle 18 | app/src/main/assets/bundle.html 19 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/.name: -------------------------------------------------------------------------------- 1 | MiniEditor -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /demo/mini-editor/android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /demo/mini-editor/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "29.0.2" 9 | 10 | defaultConfig { 11 | applicationId "com.microsoft.tscodegen.demo.minieditor" 12 | minSdkVersion 21 13 | targetSdkVersion 30 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 38 | implementation 'androidx.core:core-ktx:1.6.0' 39 | implementation 'androidx.appcompat:appcompat:1.3.1' 40 | implementation 'com.google.android.material:material:1.4.0' 41 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 42 | testImplementation 'junit:junit:4.+' 43 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 45 | 46 | implementation 'com.google.code.gson:gson:2.8.7' 47 | implementation 'com.microsoft.design:fluent-system-icons:1.1.135@aar' 48 | } -------------------------------------------------------------------------------- /demo/mini-editor/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/androidTest/java/com/microsoft/tscodegen/demo/minieditor/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.microsoft.tscodegen.demo.minieditor 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.microsoft.tscodegen.demo.minieditor", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/java/com/microsoft/tscodegen/demo/minieditor/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.microsoft.tscodegen.demo.minieditor 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.util.Base64 6 | import android.view.View 7 | import android.webkit.WebView 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.google.gson.GsonBuilder 10 | import com.microsoft.tscodegen.demo.minieditor.generated.EditorBridge 11 | import java.io.BufferedReader 12 | import java.io.IOException 13 | import java.io.InputStream 14 | import java.io.InputStreamReader 15 | 16 | private const val BUNDLE_FILENAME = "bundle.html" 17 | 18 | class MainActivity : AppCompatActivity() { 19 | private lateinit var bridge: EditorBridge 20 | 21 | @SuppressLint("SetJavaScriptEnabled") 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.activity_main) 25 | 26 | val webView = findViewById(R.id.web_view) 27 | 28 | val inputStream = baseContext.assets.open(BUNDLE_FILENAME) 29 | val htmlString = convertStreamToString(inputStream)!! 30 | 31 | val settings = webView.settings 32 | settings.javaScriptEnabled = true 33 | settings.domStorageEnabled = true 34 | settings.loadWithOverviewMode = true 35 | 36 | // Adjust from the official example: https://developer.android.com/guide/webapps/webview 37 | val encodedHtml = Base64.encodeToString(htmlString.toByteArray(), Base64.NO_PADDING) 38 | webView.loadData(encodedHtml, "text/html", "base64") 39 | 40 | val gson = GsonBuilder().create() 41 | bridge = EditorBridge(webView, gson) 42 | } 43 | 44 | private fun convertStreamToString(inputStream: InputStream): String? { 45 | val reader = BufferedReader(InputStreamReader(inputStream)) 46 | val stringBuilder = StringBuilder() 47 | var line: String? = null 48 | try { 49 | while (reader.readLine().also { line = it } != null) { 50 | stringBuilder.append(line).append('\n') 51 | } 52 | } catch (e: IOException) { 53 | e.printStackTrace() 54 | } finally { 55 | try { 56 | inputStream.close() 57 | } catch (e: IOException) { 58 | e.printStackTrace() 59 | } 60 | } 61 | return stringBuilder.toString() 62 | } 63 | 64 | fun onBoldButtonClick(view: View) { 65 | bridge.toggleBold() 66 | } 67 | 68 | fun onItalicButtonClick(view: View) { 69 | bridge.toggleItalic() 70 | } 71 | fun onUnderlineButtonClick(view: View) { 72 | bridge.toggleUnderline() 73 | } 74 | 75 | fun onInsertContentButtonClick(view: View) { 76 | bridge.insertContent("[inserted content]", true) { result -> 77 | println("[ts-gyb] result: $result") 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/java/com/microsoft/tscodegen/demo/minieditor/generated/EditorBridge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * 5 | * This file is automatically generated 6 | * Please DO NOT modify 7 | */ 8 | 9 | package com.microsoft.tscodegen.demo.minieditor.generated 10 | 11 | import com.google.gson.Gson 12 | import android.webkit.WebView 13 | 14 | interface EditorBridgeInterface { 15 | fun toggleBold() 16 | fun toggleItalic() 17 | fun toggleUnderline() 18 | fun clear() 19 | fun insertContent(content: String, newLine: Boolean?, callback: (InsertContentResult) -> Unit) 20 | } 21 | 22 | open class EditorBridge(private val webView: WebView, private val gson: Gson) : EditorBridgeInterface { 23 | 24 | override fun toggleBold() { 25 | val javascriptString = "editor.toggleBold()" 26 | println("[ts-gyb] evaluating JavaScript: $javascriptString") 27 | webView.evaluateJavascript(javascriptString){ evaluationResult -> 28 | println("[ts-gyb] JavaScript result: $evaluationResult") 29 | } 30 | } 31 | 32 | override fun toggleItalic() { 33 | val javascriptString = "editor.toggleItalic()" 34 | println("[ts-gyb] evaluating JavaScript: $javascriptString") 35 | webView.evaluateJavascript(javascriptString){ evaluationResult -> 36 | println("[ts-gyb] JavaScript result: $evaluationResult") 37 | } 38 | } 39 | 40 | override fun toggleUnderline() { 41 | val javascriptString = "editor.toggleUnderline()" 42 | println("[ts-gyb] evaluating JavaScript: $javascriptString") 43 | webView.evaluateJavascript(javascriptString){ evaluationResult -> 44 | println("[ts-gyb] JavaScript result: $evaluationResult") 45 | } 46 | } 47 | 48 | override fun clear() { 49 | val javascriptString = "editor.clear()" 50 | println("[ts-gyb] evaluating JavaScript: $javascriptString") 51 | webView.evaluateJavascript(javascriptString){ evaluationResult -> 52 | println("[ts-gyb] JavaScript result: $evaluationResult") 53 | } 54 | } 55 | 56 | data class Args_insertContent ( 57 | val content: String, 58 | val newLine: Boolean?, 59 | ) 60 | override fun insertContent(content: String, newLine: Boolean?, callback: (InsertContentResult) -> Unit) { 61 | val args = Args_insertContent( 62 | content, 63 | newLine 64 | ) 65 | val jsonString = gson.toJson(args, Args_insertContent::class.java) 66 | val javascriptString = "editor.insertContent($jsonString)" 67 | println("[ts-gyb] evaluating JavaScript: $javascriptString") 68 | webView.evaluateJavascript(javascriptString){ evaluationResult -> 69 | val result = gson.fromJson(evaluationResult, InsertContentResult::class.java) 70 | callback(result) 71 | } 72 | } 73 | } 74 | 75 | data class InsertContentResult( 76 | @JvmField val html: String, 77 | ) 78 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 26 | 27 | 32 | 33 | 40 | 41 | 42 | 49 | 50 | 51 | 58 | 59 | 60 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MiniEditor 3 | B 4 | I 5 | U 6 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /demo/mini-editor/android/app/src/test/java/com/microsoft/tscodegen/demo/minieditor/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.microsoft.tscodegen.demo.minieditor 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /demo/mini-editor/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.5.21" 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath "com.android.tools.build:gradle:4.2.2" 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | jcenter() // Warning: this repository is going to shut down soon 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } -------------------------------------------------------------------------------- /demo/mini-editor/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official -------------------------------------------------------------------------------- /demo/mini-editor/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ts-gyb/0e148e643b06014dd8112663270dca826d9739f6/demo/mini-editor/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /demo/mini-editor/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jul 28 09:35:45 CST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /demo/mini-editor/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /demo/mini-editor/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /demo/mini-editor/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "MiniEditor" 2 | include ':app' 3 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## User settings 7 | xcuserdata/ 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 10 | *.xcscmblueprint 11 | *.xccheckout 12 | 13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 14 | build/ 15 | DerivedData/ 16 | *.moved-aside 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | 26 | ## Gcc Patch 27 | /*.gcno 28 | 29 | MiniEditor/Resources/bundle.html 30 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor.xcodeproj/xcshareddata/xcschemes/MiniEditor.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/ActionButtion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class ActionButton: UIButton { 8 | var onTap: (() -> Void)? 9 | 10 | init(systemName: String, onTap: @escaping () -> Void) { 11 | 12 | super.init(frame: .zero) 13 | 14 | translatesAutoresizingMaskIntoConstraints = false 15 | 16 | self.onTap = onTap 17 | 18 | setImage(UIImage(systemName: systemName), for: .normal) 19 | addTarget(self, action: #selector(buttonDidTap), for: .touchUpInside) 20 | } 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | } 30 | 31 | private extension ActionButton { 32 | @objc func buttonDidTap() { 33 | onTap?() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | @main 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 10 | // Override point for customization after application launch. 11 | return true 12 | } 13 | 14 | // MARK: UISceneSession Lifecycle 15 | 16 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 17 | // Called when a new scene session is being created. 18 | // Use this method to select a configuration to create the new scene with. 19 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 20 | } 21 | 22 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 23 | // Called when the user discards a scene session. 24 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 25 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Generated/EditorBridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | import WebKit 9 | 10 | public class EditorBridge { 11 | weak var webView: WKWebView? 12 | 13 | private let jsonEncoder = JSONEncoder() 14 | private let jsonDecoder = JSONDecoder() 15 | 16 | init(webView: WKWebView) { 17 | self.webView = webView 18 | } 19 | 20 | public func toggleBold(completion: ((Result) -> Void)? = nil) { 21 | let javaScriptString = "editor.toggleBold" + "(" + ")" 22 | 23 | print("[ts-gyb] evaluating: \(javaScriptString)") 24 | 25 | webView?.evaluateJavaScript(javaScriptString) { evaluationResult, error in 26 | guard let completion = completion else { return } 27 | if let error = error { 28 | completion(.failure(error)) 29 | return 30 | } 31 | completion(.success(())) 32 | } 33 | } 34 | 35 | public func toggleItalic(completion: ((Result) -> Void)? = nil) { 36 | let javaScriptString = "editor.toggleItalic" + "(" + ")" 37 | 38 | print("[ts-gyb] evaluating: \(javaScriptString)") 39 | 40 | webView?.evaluateJavaScript(javaScriptString) { evaluationResult, error in 41 | guard let completion = completion else { return } 42 | if let error = error { 43 | completion(.failure(error)) 44 | return 45 | } 46 | completion(.success(())) 47 | } 48 | } 49 | 50 | public func toggleUnderline(completion: ((Result) -> Void)? = nil) { 51 | let javaScriptString = "editor.toggleUnderline" + "(" + ")" 52 | 53 | print("[ts-gyb] evaluating: \(javaScriptString)") 54 | 55 | webView?.evaluateJavaScript(javaScriptString) { evaluationResult, error in 56 | guard let completion = completion else { return } 57 | if let error = error { 58 | completion(.failure(error)) 59 | return 60 | } 61 | completion(.success(())) 62 | } 63 | } 64 | 65 | public func clear(completion: ((Result) -> Void)? = nil) { 66 | let javaScriptString = "editor.clear" + "(" + ")" 67 | 68 | print("[ts-gyb] evaluating: \(javaScriptString)") 69 | 70 | webView?.evaluateJavaScript(javaScriptString) { evaluationResult, error in 71 | guard let completion = completion else { return } 72 | if let error = error { 73 | completion(.failure(error)) 74 | return 75 | } 76 | completion(.success(())) 77 | } 78 | } 79 | 80 | public func insertContent(content: String, newLine: Bool?, completion: @escaping (Result) -> Void) { 81 | struct Args: Encodable { 82 | let content: String 83 | let newLine: Bool? 84 | } 85 | let args = Args( 86 | content: content, 87 | newLine: newLine 88 | ) 89 | let argsString = String(data: try! jsonEncoder.encode(args), encoding: .utf8)! 90 | let javaScriptString = "editor.insertContent" + "(" + "\(argsString)" + ")" 91 | 92 | print("[ts-gyb] evaluating: \(javaScriptString)") 93 | 94 | webView?.evaluateJavaScript(javaScriptString) { [unowned self]evaluationResult, error in 95 | if let error = error { 96 | completion(.failure(error)) 97 | return 98 | } 99 | let data = try! JSONSerialization.data(withJSONObject: evaluationResult!) 100 | let result = try! self.jsonDecoder.decode(InsertContentResult.self, from: data) 101 | completion(.success(result)) 102 | } 103 | } 104 | } 105 | 106 | public struct InsertContentResult: Codable { 107 | public var html: String 108 | 109 | public init(html: String) { 110 | self.html = html 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/MiniEditor.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/ts-gyb/0e148e643b06014dd8112663270dca826d9739f6/demo/mini-editor/apple/MiniEditor/Resources/.gitkeep -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 8 | var window: UIWindow? 9 | 10 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 11 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 12 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 13 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 14 | guard let windowScene = (scene as? UIWindowScene) else { return } 15 | 16 | window = UIWindow(windowScene: windowScene) 17 | 18 | window?.rootViewController = ViewController() 19 | window?.makeKeyAndVisible() 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/Toolbar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | 7 | protocol ToolbarDelegate: AnyObject { 8 | func toolbarDidToggleBold() 9 | func toolbarDidToggleItalic() 10 | func toolbarDidToggleUnderline() 11 | func toolbarDidTapInsertContent() 12 | } 13 | 14 | class Toolbar: UIView { 15 | weak var delegate: ToolbarDelegate? 16 | 17 | private lazy var stackViewContainer: UIStackView = { 18 | let stackViewContainer = UIStackView(arrangedSubviews: [ 19 | boldButton, 20 | italicButton, 21 | underlineButton, 22 | insertContentButton, 23 | ]) 24 | 25 | stackViewContainer.translatesAutoresizingMaskIntoConstraints = false 26 | 27 | stackViewContainer.spacing = 5.0 28 | stackViewContainer.distribution = .fillEqually 29 | 30 | return stackViewContainer 31 | }() 32 | 33 | private lazy var boldButton = buildButton(systemImage: "bold") { [weak self] in 34 | self?.delegate?.toolbarDidToggleBold() 35 | } 36 | 37 | private lazy var italicButton = buildButton(systemImage: "italic") { [weak self] in 38 | self?.delegate?.toolbarDidToggleItalic() 39 | } 40 | 41 | private lazy var underlineButton = buildButton(systemImage: "underline") { [weak self] in 42 | self?.delegate?.toolbarDidToggleBold() 43 | } 44 | 45 | private lazy var insertContentButton = buildButton(systemImage: "rectangle.and.pencil.and.ellipsis") { [weak self] in 46 | self?.delegate?.toolbarDidTapInsertContent() 47 | } 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | 52 | addSubview(stackViewContainer) 53 | 54 | NSLayoutConstraint.activate([ 55 | stackViewContainer.topAnchor.constraint(equalTo: topAnchor), 56 | stackViewContainer.leadingAnchor.constraint(equalTo: leadingAnchor), 57 | stackViewContainer.trailingAnchor.constraint(equalTo: trailingAnchor), 58 | stackViewContainer.bottomAnchor.constraint(equalTo: bottomAnchor), 59 | ]) 60 | 61 | stackViewContainer.backgroundColor = .secondarySystemBackground 62 | 63 | configurateFrame() 64 | setNeedsLayout() 65 | } 66 | 67 | @available(*, unavailable) 68 | required init?(coder _: NSCoder) { 69 | fatalError("init(coder:) has not been implemented") 70 | } 71 | 72 | override func layoutSubviews() { 73 | super.layoutSubviews() 74 | configurateFrame() 75 | } 76 | } 77 | 78 | private extension Toolbar { 79 | func buildButton(systemImage: String, onTap: @escaping () -> Void) -> UIButton { 80 | ActionButton(systemName: systemImage, onTap: onTap) 81 | } 82 | 83 | func configurateFrame() { 84 | frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import WebKit 7 | 8 | class ViewController: UIViewController { 9 | private lazy var webView: WKWebView = { 10 | let webView = WebView() 11 | webView.translatesAutoresizingMaskIntoConstraints = false 12 | 13 | let toolbar = Toolbar() 14 | toolbar.delegate = self 15 | 16 | webView.toolbar = toolbar 17 | 18 | return webView 19 | }() 20 | 21 | private lazy var editorBridge = EditorBridge(webView: webView) 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | // Do any additional setup after loading the view. 26 | 27 | view.backgroundColor = .systemBackground 28 | 29 | view.addSubview(webView) 30 | 31 | NSLayoutConstraint.activate([ 32 | webView.topAnchor.constraint(equalTo: view.topAnchor), 33 | webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 34 | webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 35 | webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 36 | ]) 37 | 38 | loadJavaScriptBundle() 39 | } 40 | } 41 | 42 | extension ViewController: ToolbarDelegate { 43 | func toolbarDidToggleBold() { 44 | editorBridge.toggleBold { [unowned self] result in 45 | self.handleVoidResult(result) 46 | } 47 | } 48 | 49 | func toolbarDidToggleItalic() { 50 | editorBridge.toggleItalic { [unowned self] result in 51 | self.handleVoidResult(result) 52 | } 53 | } 54 | 55 | func toolbarDidToggleUnderline() { 56 | editorBridge.toggleUnderline { [unowned self] result in 57 | self.handleVoidResult(result) 58 | } 59 | } 60 | 61 | func toolbarDidTapInsertContent() { 62 | editorBridge.insertContent( 63 | content: "did tap insert content", 64 | newLine: true 65 | ) { result in 66 | switch result { 67 | case .success(let contentString): 68 | print("[ts-gyb] contentString after insertContent(): \(contentString)") 69 | case .failure(let error): 70 | assertionFailure("\(error)") 71 | } 72 | } 73 | } 74 | } 75 | 76 | private extension ViewController { 77 | func loadJavaScriptBundle() { 78 | let bundleURL = Bundle.main.url(forResource: "bundle", withExtension: "html")! 79 | let javaScriptContent = try! String(contentsOf: bundleURL, encoding: .utf8) 80 | 81 | webView.loadHTMLString(javaScriptContent, baseURL: nil) 82 | } 83 | 84 | func handleVoidResult(_ result: Result) { 85 | switch result { 86 | case .success: 87 | break 88 | case .failure(let error): 89 | assertionFailure("\(error)") 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /demo/mini-editor/apple/MiniEditor/WebView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | import UIKit 6 | import WebKit 7 | 8 | class WebView: WKWebView { 9 | var toolbar: Toolbar? 10 | 11 | override var inputAccessoryView: UIView? { 12 | toolbar 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo/mini-editor/web/code-templates/kotlin-named-type.mustache: -------------------------------------------------------------------------------- 1 | {{#custom}} 2 | data class {{typeName}}( 3 | {{#members}} 4 | @JvmField val {{name}}: {{type}}, 5 | {{/members}} 6 | {{#staticMembers}} 7 | @JvmField val {{name}}: {{type}} = {{{value}}}, 8 | {{/staticMembers}} 9 | ) 10 | {{/custom}} 11 | {{#enum}} 12 | {{#isNumberType}} 13 | enum class {{typeName}}(val value: Int) { 14 | {{#members}} 15 | {{key}}({{{value}}}){{^last}},{{/last}}{{#last}};{{/last}} 16 | {{/members}} 17 | 18 | companion object { 19 | fun find(value: Int) = values().find { it.value == value } 20 | } 21 | } 22 | 23 | class {{typeName}}TypeAdapter : JsonSerializer<{{typeName}}>, JsonDeserializer<{{typeName}}> { 24 | override fun serialize(obj: {{typeName}}, type: Type, context: JsonSerializationContext): JsonElement { 25 | return JsonPrimitive(obj.value) 26 | } 27 | 28 | override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): {{typeName}}? { 29 | return {{typeName}}.find(json.asInt) 30 | } 31 | } 32 | {{/isNumberType}} 33 | {{#isStringType}} 34 | enum class {{typeName}} { 35 | {{#members}} 36 | @SerializedName({{{value}}}) {{key}}{{^last}},{{/last}} 37 | {{/members}} 38 | } 39 | {{/isStringType}} 40 | {{/enum}} 41 | -------------------------------------------------------------------------------- /demo/mini-editor/web/code-templates/kotlin-named-types.mustache: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. 3 | * Microsoft Corporation. All rights reserved. 4 | * 5 | * 6 | * This file is automatically generated 7 | * Please DO NOT modify 8 | */ 9 | 10 | package com.microsoft.office.outlook.rooster.web.bridge 11 | 12 | import java.lang.reflect.Type 13 | import com.google.gson.JsonDeserializationContext 14 | import com.google.gson.JsonDeserializer 15 | import com.google.gson.JsonElement 16 | import com.google.gson.JsonPrimitive 17 | import com.google.gson.JsonSerializationContext 18 | import com.google.gson.JsonSerializer 19 | import com.google.gson.annotations.SerializedName 20 | 21 | {{#.}} 22 | 23 | {{> kotlin-named-type}} 24 | {{/.}} 25 | -------------------------------------------------------------------------------- /demo/mini-editor/web/code-templates/kotlin.mustache: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * 4 | * 5 | * This file is automatically generated 6 | * Please DO NOT modify 7 | */ 8 | 9 | package com.microsoft.tscodegen.demo.minieditor.generated 10 | 11 | import com.google.gson.Gson 12 | import android.webkit.WebView 13 | 14 | interface {{moduleName}}Interface { 15 | {{#methods}} 16 | fun {{methodName}}({{{parametersDeclaration}}}{{#returnType}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}callback: ({{{returnType}}}) -> Unit{{/returnType}}) 17 | {{/methods}} 18 | } 19 | 20 | open class {{moduleName}}(private val webView: WebView, private val gson: Gson) : {{moduleName}}Interface { 21 | {{#methods}} 22 | 23 | {{#parameters.length}} 24 | data class Args_{{methodName}} ( 25 | {{#parameters}} 26 | val {{name}}: {{type}}, 27 | {{/parameters}} 28 | ) 29 | {{/parameters.length}} 30 | override fun {{methodName}}({{{parametersDeclaration}}}{{#returnType}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}callback: ({{{returnType}}}) -> Unit{{/returnType}}) { 31 | {{#parameters.length}} 32 | val args = Args_{{methodName}}( 33 | {{#parameters}} 34 | {{name}}{{^last}},{{/last}} 35 | {{/parameters}} 36 | ) 37 | val jsonString = gson.toJson(args, Args_{{methodName}}::class.java) 38 | {{/parameters.length}} 39 | val javascriptString = "{{customTags.invokePath}}.{{methodName}}({{#parameters.length}}$jsonString{{/parameters.length}})" 40 | println("[ts-gyb] evaluating JavaScript: $javascriptString") 41 | webView.evaluateJavascript(javascriptString){ evaluationResult -> 42 | {{#returnType}} 43 | val result = gson.fromJson(evaluationResult, {{{returnType}}}::class.java) 44 | callback(result) 45 | {{/returnType}} 46 | {{^returnType}} 47 | println("[ts-gyb] JavaScript result: $evaluationResult") 48 | {{/returnType}} 49 | } 50 | } 51 | {{/methods}} 52 | } 53 | {{#associatedTypes}} 54 | 55 | {{> kotlin-named-type}} 56 | {{/associatedTypes}} 57 | -------------------------------------------------------------------------------- /demo/mini-editor/web/code-templates/swift-named-type.mustache: -------------------------------------------------------------------------------- 1 | {{#custom}} 2 | {{#documentationLines}} 3 | ///{{{.}}} 4 | {{/documentationLines}} 5 | public struct {{typeName}}: Codable { 6 | {{#members}} 7 | {{#documentationLines}} 8 | ///{{{.}}} 9 | {{/documentationLines}} 10 | public var {{name}}: {{type}} 11 | {{/members}} 12 | {{#staticMembers}} 13 | {{#documentationLines}} 14 | ///{{{.}}} 15 | {{/documentationLines}} 16 | private var {{name}}: {{type}} = {{{value}}} 17 | {{/staticMembers}} 18 | 19 | public init({{#members}}{{name}}: {{type}}{{^last}}, {{/last}}{{/members}}) { 20 | {{#members}} 21 | self.{{name}} = {{name}} 22 | {{/members}} 23 | } 24 | } 25 | {{/custom}} 26 | {{#enum}} 27 | {{#documentationLines}} 28 | ///{{{.}}} 29 | {{/documentationLines}} 30 | public enum {{typeName}}: {{valueType}}, Codable { 31 | {{#members}} 32 | {{#documentationLines}} 33 | ///{{{.}}} 34 | {{/documentationLines}} 35 | case {{key}} = {{{value}}} 36 | {{/members}} 37 | } 38 | {{/enum}} 39 | -------------------------------------------------------------------------------- /demo/mini-editor/web/code-templates/swift.mustache: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | import WebKit 9 | 10 | public class {{moduleName}} { 11 | {{#customTags.privateDispatcher}}private {{/customTags.privateDispatcher}}weak var webView: WKWebView? 12 | 13 | private let jsonEncoder = JSONEncoder() 14 | private let jsonDecoder = JSONDecoder() 15 | 16 | init(webView: WKWebView) { 17 | self.webView = webView 18 | } 19 | {{#methods}} 20 | 21 | {{#documentationLines}} 22 | ///{{{.}}} 23 | {{/documentationLines}} 24 | public func {{methodName}}({{parametersDeclaration}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}completion: {{#returnType}}@escaping (Result<{{returnType}}, Error>) -> Void{{/returnType}}{{^returnType}}((Result) -> Void)? = nil{{/returnType}}) { 25 | {{#parameters.length}} 26 | struct Args: Encodable { 27 | {{#parameters}} 28 | let {{name}}: {{type}} 29 | {{/parameters}} 30 | } 31 | {{/parameters.length}} 32 | {{#parameters.length}} 33 | let args = Args( 34 | {{#parameters}} 35 | {{name}}: {{name}}{{^last}},{{/last}} 36 | {{/parameters}} 37 | ) 38 | let argsString = String(data: try! jsonEncoder.encode(args), encoding: .utf8)! 39 | {{/parameters.length}} 40 | let javaScriptString = "{{customTags.invokePath}}.{{methodName}}" + "(" {{#parameters.length}}+ "\(argsString)"{{/parameters.length}} + ")" 41 | 42 | print("[ts-gyb] evaluating: \(javaScriptString)") 43 | 44 | webView?.evaluateJavaScript(javaScriptString) { {{#returnType}}[unowned self]{{/returnType}}evaluationResult, error in 45 | {{^returnType}} 46 | guard let completion = completion else { return } 47 | {{/returnType}} 48 | if let error = error { 49 | completion(.failure(error)) 50 | return 51 | } 52 | {{#returnType}} 53 | let data = try! JSONSerialization.data(withJSONObject: evaluationResult!) 54 | let result = try! self.jsonDecoder.decode({{returnType}}.self, from: data) 55 | completion(.success(result)) 56 | {{/returnType}} 57 | {{^returnType}} 58 | completion(.success(())) 59 | {{/returnType}} 60 | } 61 | } 62 | {{/methods}} 63 | } 64 | {{#associatedTypes}} 65 | 66 | {{> swift-named-type}} 67 | {{/associatedTypes}} 68 | -------------------------------------------------------------------------------- /demo/mini-editor/web/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "parsing": { 3 | "targets": { 4 | "api": { 5 | "source": ["src/editor/IEditor.ts"] 6 | } 7 | }, 8 | "predefinedTypes": [ 9 | "CodeGen_Int" 10 | ], 11 | "defaultCustomTags": {}, 12 | "dropInterfaceIPrefix": true 13 | }, 14 | "rendering": { 15 | "swift": { 16 | "renders": [ 17 | { 18 | "target": "api", 19 | "template": "code-templates/swift.mustache", 20 | "outputPath": "../apple/MiniEditor/Generated" 21 | } 22 | ], 23 | "namedTypesTemplatePath": "../../../example-templates/swift-named-types.mustache", 24 | "namedTypesOutputPath": "../apple/MiniEditor/Generated/WebEditorTypes.swift", 25 | "typeNameMap": { 26 | "CodeGen_Int": "Int" 27 | } 28 | }, 29 | "kotlin": { 30 | "renders": [ 31 | { 32 | "target": "api", 33 | "template": "code-templates/kotlin.mustache", 34 | "outputPath": "../android/app/src/main/java/com/microsoft/tscodegen/demo/minieditor/generated" 35 | } 36 | ], 37 | "namedTypesTemplatePath": "../../../example-templates/kotlin-named-type.mustache", 38 | "namedTypesOutputPath": "../android/app/src/main/java/com/microsoft/tscodegen/demo/minieditor/generated/BridgeTypes.kt", 39 | "typeNameMap": { 40 | "CodeGen_Int": "Int" 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /demo/mini-editor/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-editor-web", 3 | "version": "0.0.1", 4 | "description": "An end-to-end example of ts-gyb", 5 | "main": "src/main.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build:web": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack-config.json\" webpack --env target='web'", 9 | "build:apple": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack-config.json\" webpack --env target='apple'", 10 | "build:android": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack-config.json\" webpack --env target='android'", 11 | "serve:web": "cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack-config.json\" webpack serve --env target='web'" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "cross-env": "^7.0.3", 17 | "html-inline-css-webpack-plugin": "^1.11.1", 18 | "html-inline-script-webpack-plugin": "^2.0.2", 19 | "html-webpack-plugin": "^5.3.2", 20 | "mini-css-extract-plugin": "^2.1.0", 21 | "ts-loader": "^9.2.4", 22 | "tsconfig-paths": "^3.10.1", 23 | "typescript": "^4.3.5", 24 | "webpack": "^5.46.0", 25 | "webpack-cli": "^4.7.2", 26 | "webpack-dev-server": "^4.11.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/mini-editor/web/src/editor/Editor.ts: -------------------------------------------------------------------------------- 1 | import { IEditor, IInsertContentResult } from "./IEditor"; 2 | 3 | 4 | export class Editor implements IEditor { 5 | constructor(private contentEditableDiv: HTMLDivElement) { } 6 | 7 | toggleBold(): void { 8 | document.execCommand('bold'); 9 | } 10 | 11 | toggleItalic(): void { 12 | document.execCommand('italic'); 13 | } 14 | 15 | toggleUnderline(): void { 16 | document.execCommand('underline'); 17 | } 18 | 19 | focus(): void { 20 | this.contentEditableDiv.focus(); 21 | } 22 | 23 | clear(): void { 24 | this.contentEditableDiv.innerHTML = ''; 25 | this.focus(); 26 | } 27 | 28 | insertContent({ content, newLine }: { content: string; newLine?: boolean | undefined; }): IInsertContentResult { 29 | const contentBeingInserted = newLine ? `${content}\n` : content; 30 | document.execCommand('insertHTML', false, contentBeingInserted); 31 | 32 | return { html: this.contentEditableDiv.innerHTML }; 33 | } 34 | } -------------------------------------------------------------------------------- /demo/mini-editor/web/src/editor/IEditor.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @shouldExport true 4 | * @invokePath editor 5 | * @overrideModuleName EditorBridge 6 | */ 7 | export interface IEditor { 8 | toggleBold(): void; 9 | toggleItalic(): void; 10 | toggleUnderline(): void; 11 | 12 | clear(): void; 13 | 14 | insertContent({ content, newLine }: { content: string; newLine?: boolean }): IInsertContentResult; 15 | } 16 | 17 | export interface IInsertContentResult { 18 | html: string; 19 | } -------------------------------------------------------------------------------- /demo/mini-editor/web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mini Editor 9 | 10 | 11 | 12 |
13 | Hello, I'm a mini editor (⁎⁍̴̛ᴗ⁍̴̛⁎) 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /demo/mini-editor/web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from "./editor/Editor"; 2 | import { onReady } from "./utils/onReady"; 3 | 4 | onReady(() => { 5 | const editor = new Editor(document.getElementById("mini-editor") as HTMLDivElement); 6 | console.log('Editor is ready:', editor); 7 | 8 | // Mount `editor` into global scope 9 | window.editor = editor; 10 | 11 | requestAnimationFrame(() => { 12 | editor.focus(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /demo/mini-editor/web/src/types/types.d.ts: -------------------------------------------------------------------------------- 1 | import { IEditor } from '../editor/IEditor'; 2 | 3 | declare global { 4 | interface Window { 5 | editor: IEditor; 6 | } 7 | } -------------------------------------------------------------------------------- /demo/mini-editor/web/src/utils/onReady.ts: -------------------------------------------------------------------------------- 1 | export function onReady(cb: () => void): void { 2 | if (document.readyState !== 'loading') { 3 | setTimeout(cb, 0); 4 | } else { 5 | document.addEventListener('DOMContentLoaded', cb); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/mini-editor/web/tsconfig-for-webpack-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo/mini-editor/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": [ 4 | "src/**/*.ts" 5 | ], 6 | "compilerOptions": { 7 | "lib": [ 8 | "dom", 9 | "dom.iterable", 10 | "es5", 11 | "es6", 12 | "es2015.promise", 13 | "es2016" 14 | ], 15 | "typeRoots": [ 16 | "src/types" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /demo/mini-editor/web/webpack.config.ts: -------------------------------------------------------------------------------- 1 | // TypeScript based configuration for webpack. 2 | // Reference: https://webpack.js.org/configuration/configuration-languages/#typescript 3 | 4 | import path from 'path'; 5 | import * as webpack from 'webpack'; 6 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 7 | import HTMLInlineCSSWebpackPlugin from 'html-inline-css-webpack-plugin'; 8 | import HtmlInlineScriptPlugin from 'html-inline-script-webpack-plugin'; 9 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 10 | 11 | const webDistPath = path.resolve(__dirname, 'dist'); 12 | const sourcePath = path.resolve(__dirname, 'src'); 13 | 14 | const appleDistPath = path.resolve(__dirname, '..', 'apple/MiniEditor/Resources'); 15 | const androidDistPath = path.resolve(__dirname, '..', 'android/app/src/main/assets'); 16 | 17 | enum SupportedTarget { 18 | web = 'web', 19 | apple = 'apple', 20 | android = 'android', 21 | } 22 | 23 | interface WebpackEnvironment { 24 | target: SupportedTarget; 25 | port?: number; 26 | } 27 | 28 | function buildConfig(env: WebpackEnvironment): webpack.Configuration | webpack.WebpackOptionsNormalized { 29 | const isProductionBuild = process.env.NODE_ENV === 'production'; 30 | 31 | const distPath = function (): string { 32 | switch (env.target) { 33 | case SupportedTarget.web: 34 | return webDistPath; 35 | case SupportedTarget.apple: 36 | return appleDistPath; 37 | case SupportedTarget.android: 38 | return androidDistPath; 39 | default: 40 | throw new Error(`Unsupported target: ${env.target}`); 41 | } 42 | }(); 43 | 44 | return { 45 | mode: isProductionBuild ? 'production' : 'development', 46 | entry: `${sourcePath}/main.ts`, 47 | devServer: { 48 | static: { 49 | directory: distPath, 50 | serveIndex: true, 51 | }, 52 | compress: true, 53 | port: env.port ?? 9000, 54 | }, 55 | output: { 56 | path: distPath, 57 | filename: 'bundle.js', 58 | }, 59 | resolve: { 60 | extensions: ['.ts', '.tsx', '.js'], 61 | }, 62 | module: { 63 | rules: [ 64 | { 65 | test: /\.tsx?$/, 66 | include: [ 67 | sourcePath, 68 | ], 69 | use: [ 70 | { 71 | loader: 'ts-loader', 72 | options: { 73 | configFile: 'tsconfig.json', 74 | compilerOptions: { 75 | sourceMap: !isProductionBuild, 76 | }, 77 | }, 78 | }, 79 | ], 80 | } 81 | ], 82 | }, 83 | plugins: [ 84 | new MiniCssExtractPlugin({ 85 | filename: '[name].css', 86 | chunkFilename: '[id].css', 87 | }), 88 | // Note: For mobile platforms, we need to inline all JavaScript 89 | // and CSS resources into a single file for simplicity. 90 | new HtmlWebpackPlugin({ 91 | template: `${sourcePath}/index.html`, 92 | filename: './bundle.html', 93 | inlineSource: '.(js|css)$', 94 | }), 95 | new HTMLInlineCSSWebpackPlugin(), 96 | new HtmlInlineScriptPlugin(), 97 | ] 98 | }; 99 | } 100 | 101 | export default (env) => buildConfig(env);; 102 | -------------------------------------------------------------------------------- /documentation/generated/interfaces/Configuration.md: -------------------------------------------------------------------------------- 1 | [ts-gyb](../README.md) / [Exports](../modules.md) / Configuration 2 | 3 | # Interface: Configuration 4 | 5 | Root configuration 6 | 7 | ## Table of contents 8 | 9 | ### Properties 10 | 11 | - [parsing](Configuration.md#parsing) 12 | - [rendering](Configuration.md#rendering) 13 | 14 | ## Properties 15 | 16 | ### parsing 17 | 18 | • **parsing**: [`ParseConfiguration`](ParseConfiguration.md) 19 | 20 | Parser configuration 21 | 22 | ___ 23 | 24 | ### rendering 25 | 26 | • **rendering**: [`LanguageRenderingConfiguration`](LanguageRenderingConfiguration.md) 27 | 28 | Code generation configuration for different languages 29 | -------------------------------------------------------------------------------- /documentation/generated/interfaces/LanguageRenderingConfiguration.md: -------------------------------------------------------------------------------- 1 | [ts-gyb](../README.md) / [Exports](../modules.md) / LanguageRenderingConfiguration 2 | 3 | # Interface: LanguageRenderingConfiguration 4 | 5 | Language rendering configuration 6 | 7 | ## Table of contents 8 | 9 | ### Properties 10 | 11 | - [kotlin](LanguageRenderingConfiguration.md#kotlin) 12 | - [swift](LanguageRenderingConfiguration.md#swift) 13 | 14 | ## Properties 15 | 16 | ### kotlin 17 | 18 | • `Optional` **kotlin**: [`RenderConfiguration`](RenderConfiguration.md) 19 | 20 | Kotlin renderer configuration 21 | 22 | ___ 23 | 24 | ### swift 25 | 26 | • `Optional` **swift**: [`RenderConfiguration`](RenderConfiguration.md) 27 | 28 | Swift renderer configuration 29 | -------------------------------------------------------------------------------- /documentation/generated/interfaces/ParseConfiguration.md: -------------------------------------------------------------------------------- 1 | [ts-gyb](../README.md) / [Exports](../modules.md) / ParseConfiguration 2 | 3 | # Interface: ParseConfiguration 4 | 5 | Parser configuration 6 | 7 | ## Table of contents 8 | 9 | ### Properties 10 | 11 | - [defaultCustomTags](ParseConfiguration.md#defaultcustomtags) 12 | - [dropInterfaceIPrefix](ParseConfiguration.md#dropinterfaceiprefix) 13 | - [predefinedTypes](ParseConfiguration.md#predefinedtypes) 14 | - [skipInvalidMethods](ParseConfiguration.md#skipinvalidmethods) 15 | - [source](ParseConfiguration.md#source) 16 | 17 | ## Properties 18 | 19 | ### defaultCustomTags 20 | 21 | • `Optional` **defaultCustomTags**: `Record`<`string`, `unknown`\> 22 | 23 | Custom tags for code generation in mustache and its default value. 24 | 25 | ___ 26 | 27 | ### dropInterfaceIPrefix 28 | 29 | • `Optional` **dropInterfaceIPrefix**: `boolean` 30 | 31 | Drop the `I` prefix for TypeScript interfaces. 32 | This only works for types used as method parameters or return value. 33 | 34 | ___ 35 | 36 | ### predefinedTypes 37 | 38 | • `Optional` **predefinedTypes**: `string`[] 39 | 40 | Names for pre-defined types. 41 | For example, `CodeGen_Int` for mapping for `number` to integers. 42 | 43 | ___ 44 | 45 | ### skipInvalidMethods 46 | 47 | • `Optional` **skipInvalidMethods**: `boolean` 48 | 49 | Skip the code generation for invalid methods. If `false`, the code generation will fail when encounter an unsupported type. 50 | 51 | ___ 52 | 53 | ### targets 54 | 55 | • **targets**: `Record` 56 | 57 | Describe the target interfaces to be parsed, the key is the name of the target, and the value is an object including the below properties: 58 | 59 | #### source 60 | 61 | • **source**: `string[]` 62 | 63 | Scoped source file paths. The array of the source file paths for one target. [Glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)) are allowed. 64 | If it is a relative path, it will be resolved based on the configuration file path. 65 | 66 | For example, `["src/api/IEditor.ts", "src/bridge/*.ts"]` 67 | 68 | #### exportedInterfaceBases 69 | 70 | • `Optional` **exportedInterfaceBases**: `string`[] 71 | 72 | Interface names for detecting exported modules. If defined, only interfaces that extends the specified interfaces will be parsed. 73 | If not defined, interfaces with JSDoc tag `@shouldExport true` would be parsed. 74 | For example, set it to `["ExportedInterface"]`, all such interfaces would be exported: 75 | ```ts 76 | interface SomeInterface extends ExportedInterface {} 77 | ``` 78 | 79 | #### tsconfigPath 80 | 81 | • `Optional` **tsconfigPath**: `string` 82 | 83 | Path to the tsconfig.json file. If not defined, the default tsconfig.json file will be used. -------------------------------------------------------------------------------- /documentation/generated/interfaces/RenderConfiguration.md: -------------------------------------------------------------------------------- 1 | [ts-gyb](../README.md) / [Exports](../modules.md) / RenderConfiguration 2 | 3 | # Interface: RenderConfiguration 4 | 5 | Renderer configuration 6 | 7 | ## Table of contents 8 | 9 | ### Properties 10 | 11 | - [namedTypesOutputPath](RenderConfiguration.md#namedtypesoutputpath) 12 | - [namedTypesTemplatePath](RenderConfiguration.md#namedtypestemplatepath) 13 | - [outputPath](RenderConfiguration.md#outputpath) 14 | - [templates](RenderConfiguration.md#templates) 15 | - [typeNameMap](RenderConfiguration.md#typenamemap) 16 | 17 | ## Properties 18 | 19 | ### namedTypesOutputPath 20 | 21 | • **namedTypesOutputPath**: `string` 22 | 23 | Output path for named types. 24 | If it is a relative path, it will be resolved based on the configuration file path. 25 | 26 | ___ 27 | 28 | ### namedTypesTemplatePath 29 | 30 | • **namedTypesTemplatePath**: `string` 31 | 32 | Template path for named types. Must be a mustache template. 33 | If it is a relative path, it will be resolved based on the configuration file path. 34 | 35 | For example, `code-templates/named-types.mustache`. 36 | 37 | ___ 38 | 39 | ### outputPath 40 | 41 | • **outputPath**: `Record`<`string`, `string`\> 42 | 43 | Scoped output directories or paths. The key is the scope name and the value is the output directory or file path. 44 | 45 | If it is a relative path, it will be resolved based on the configuration file path. 46 | 47 | For example, `{ "api": "../ios/AppTarget/Generated" }` 48 | 49 | ___ 50 | 51 | ### templates 52 | 53 | • **templates**: `Record`<`string`, `string`\> 54 | 55 | Scoped template file paths. The key is the scope name and the value is the template file path. 56 | If it is a relative path, it will be resolved based on the configuration file path. 57 | 58 | ___ 59 | 60 | ### typeNameMap 61 | 62 | • `Optional` **typeNameMap**: `Record`<`string`, `string`\> 63 | 64 | The mapping from `predefinedTypes` to the existing types in the target language (Kotlin/Swift). 65 | 66 | For example, `{ "CodeGen_Int": "Int" }`. 67 | -------------------------------------------------------------------------------- /documentation/predefined-type.md: -------------------------------------------------------------------------------- 1 | # Predefined Type 2 | 3 | To create predefined types, such as mapping `number` in TypeScript to `Int` in Swift, we need to: 4 | 5 | 1. Create a TypeScript interface or type alias. Example: 6 | 7 | ```ts 8 | type CodeGen_Int = number; 9 | ``` 10 | 11 | 2. Specify this custom type in the configuration file. Example: 12 | 13 | ```json 14 | "parsing": { 15 | "predefinedTypes": [ 16 | "CodeGen_Int" 17 | ] 18 | ``` 19 | 20 | 3. Make sure the mapping is also defined for the target language renderer. Example: 21 | 22 | ```json 23 | "rendering": { 24 | "swift": { 25 | "typeNameMap": { 26 | "CodeGen_Int": "Int" 27 | } 28 | ``` 29 | 30 | For full example, please refer to . 31 | -------------------------------------------------------------------------------- /documentation/template-guide.md: -------------------------------------------------------------------------------- 1 | # Template Guide 2 | 3 | ts-gyb uses [mustache](http://mustache.github.io) template to generate code. Refer to [mustache Manual](http://mustache.github.io/mustache.5.html) for supported syntax. 4 | 5 | ## Required templates 6 | 7 | ts-gyb needs two templates: module template and named type template. 8 | 9 | ### Module template 10 | 11 | ts-gyb uses this template to generate a file for every module. Typically, generated file includes a module class and all types used in the method parameters and return values of the module. 12 | 13 | ### Named type template 14 | 15 | When a TypeScript `interface` or an `enum` is used by more than one module, it is not suitable to place the generated type in any module file. ts-gyb uses this template to generate a single file that hosts all shared TypeScript types found in method parameters and return types. 16 | 17 | ## Variables 18 | 19 | ts-gyb defines some variables that can be directly used in templates. 20 | 21 | ### Variables in templates 22 | 23 | - **Module template**: this template has access to all properties of [`Module`](#module). For example, you can use `{{moduleName}}` to render the name of the module. If the [`outputPath`](generated/interfaces/RenderConfiguration.md#outputPath) is a file, this template would have access to an array of `Module`. 24 | - **Named type template**: this template has only one array variable of type [`NamedType`](#namedtype) which includes all named types. For example, use `{{#.}}{{typeName}}{{/.}}` to enumerate the array and get all type names. 25 | 26 | Both `associatedTypes` in Module template and the variable of Named type template is an array of [`NamedType`](#namedtype). It is recommended to create a template to render a single `NamedType`, then import the template in other templates. 27 | 28 | ### Data structure 29 | 30 | #### `Module` 31 | 32 | - `moduleName`: the name of the module. 33 | - `members`: an array of [`Field`](#field). It includes all properties of the module. 34 | - `methods`: an array of [`Method`](#method). 35 | - `associatedTypes`: an array of [`NamedType`](#namedtype). This includes all types used only in this module. 36 | - `customTags`: an object of all custom defined values in module interface. Refer to [Custom tags](interface-guide.md#custom-tags) for how to define these tags. For example, you can access `@foobar { "key": "value" }` via `{{customTags.foobar.key}}`. 37 | - `documentationLines`: an array of documentation text divided to string lines. It is divided for easier use with mustache. 38 | 39 | #### `Method` 40 | 41 | - `methodName`: the name of the method. 42 | - `parameters`: an array of [`Parameter`](#parameter) to include all parameters of the method. 43 | - `parametersDeclaration`: a single line parameters declaration. It can be directly used inside `()` to declare all parameters of this method. 44 | - `returnType`: a string to represent the return type of the method. 45 | - `nonOptionalReturnType`: a string to represent the return type but stripped optional syntax. For non optional types, this would be the same as `returnType`. 46 | - `documentationLines`: an array of documentation text divided to string lines. It is divided for easier use with mustache. 47 | 48 | #### `Parameter` 49 | 50 | - `name`: the name of the parameter. 51 | - `type`: a string to represent the type of the parameter. 52 | - `last`: a boolean to indicate whether this is the last parameter. It is for convenience of writing mustache templates. 53 | 54 | #### `NamedType` 55 | 56 | - `custom`: a boolean to indicate whether this is an interface type. 57 | - `enum`: a boolean to indicate whether this is an enum type. 58 | 59 | For interface type: 60 | 61 | - `typeName`: the name of the type. 62 | - `members`: an array of [`Field`](#field). It includes all members of the interface type. 63 | - `documentationLines`: an array of documentation text divided to string lines. It is divided for easier use with mustache. 64 | 65 | For enum type: 66 | 67 | - `typeName`: the name of the type. 68 | - `valueType`: a string to represent the type of the raw value of the enum. 69 | - `isNumberType`: a boolean to indicate whether this is a numeric enum. 70 | - `isStringType`: a boolean to indicate whether this is a string enum. 71 | - `members`: an array of [`EnumMember`](#enummember). It includes all members of the enum type. 72 | - `documentationLines`: an array of documentation text divided to string lines. It is divided for easier use with mustache. 73 | 74 | #### `Field` 75 | 76 | - `name`: the name of the interface member. 77 | - `type`: a string to represent the type of the member. 78 | - `documentationLines`: an array of documentation text divided to string lines. It is divided for easier use with mustache. 79 | - `last`: a boolean to indicate whether this is the last member. It is for convenience of writing mustache templates. 80 | 81 | #### `EnumMember` 82 | 83 | - `key`: the key of the enum member 84 | - `value`: a string to represent the value of the enum member 85 | - `documentationLines`: an array of documentation text divided to string lines. It is divided for easier use with mustache. 86 | - `last`: a boolean to indicate whether this is the last member. It is for convenience of writing mustache templates. 87 | -------------------------------------------------------------------------------- /example-templates/kotlin-bridge.mustache: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. 3 | * Microsoft Corporation. All rights reserved. 4 | * 5 | * 6 | * This file is automatically generated 7 | * Please DO NOT modify 8 | */ 9 | 10 | package com.microsoft.office.outlook.rooster.web.bridge 11 | 12 | import com.google.gson.Gson 13 | import com.microsoft.office.outlook.rooster.Callback 14 | import com.microsoft.office.outlook.rooster.web.JsBridge 15 | import com.microsoft.office.outlook.rooster.web.WebEditor 16 | {{#associatedTypes.length}} 17 | import java.lang.reflect.Type 18 | import com.google.gson.JsonDeserializationContext 19 | import com.google.gson.JsonDeserializer 20 | import com.google.gson.JsonElement 21 | import com.google.gson.JsonPrimitive 22 | import com.google.gson.JsonSerializationContext 23 | import com.google.gson.JsonSerializer 24 | import com.google.gson.annotations.SerializedName 25 | {{/associatedTypes.length}} 26 | 27 | interface {{moduleName}}Bridge { 28 | {{#methods}} 29 | fun {{methodName}}({{{parametersDeclaration}}}{{#returnType}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}callback: Callback<{{{returnType}}}>{{/returnType}}) 30 | {{/methods}} 31 | } 32 | 33 | open class {{moduleName}}Bridge(editor: WebEditor, gson: Gson) : JsBridge(editor, gson, "{{customTags.invokePath}}"), {{moduleName}}Bridge { 34 | {{#methods}} 35 | 36 | override fun {{methodName}}({{{parametersDeclaration}}}{{#returnType}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}callback: Callback<{{{returnType}}}>{{/returnType}}) { 37 | {{#returnType}}executeJsForResponse({{{nonOptionalReturnType}}}::class.java, {{/returnType}}{{^returnType}}executeJs({{/returnType}}"{{methodName}}"{{#returnType}}, callback{{/returnType}}{{#parametersDeclaration.length}}, mapOf({{/parametersDeclaration.length}}{{^parameters}}){{/parameters}} 38 | {{#parameters}} 39 | "{{name}}" to {{name}} 40 | {{/parameters}} 41 | {{#parametersDeclaration.length}} 42 | )) 43 | {{/parametersDeclaration.length}} 44 | } 45 | {{/methods}} 46 | } 47 | {{#associatedTypes}} 48 | 49 | {{> kotlin-named-type}} 50 | {{/associatedTypes}} 51 | -------------------------------------------------------------------------------- /example-templates/kotlin-named-type.mustache: -------------------------------------------------------------------------------- 1 | {{#custom}} 2 | data class {{typeName}}( 3 | {{#members}} 4 | @JvmField val {{name}}: {{type}}{{#defaultValue}} = {{defaultValue}}{{/defaultValue}}, 5 | {{/members}} 6 | {{#staticMembers}} 7 | @JvmField val {{name}}: {{type}} = {{{value}}}, 8 | {{/staticMembers}} 9 | ) 10 | {{/custom}} 11 | {{#enum}} 12 | {{#isNumberType}} 13 | enum class {{typeName}}(val value: Int) { 14 | {{#members}} 15 | {{key}}({{{value}}}){{^last}},{{/last}}{{#last}};{{/last}} 16 | {{/members}} 17 | 18 | companion object { 19 | fun find(value: Int) = values().find { it.value == value } 20 | } 21 | } 22 | 23 | class {{typeName}}TypeAdapter : JsonSerializer<{{typeName}}>, JsonDeserializer<{{typeName}}> { 24 | override fun serialize(obj: {{typeName}}, type: Type, context: JsonSerializationContext): JsonElement { 25 | return JsonPrimitive(obj.value) 26 | } 27 | 28 | override fun deserialize(json: JsonElement, type: Type, context: JsonDeserializationContext): {{typeName}}? { 29 | return {{typeName}}.find(json.asInt) 30 | } 31 | } 32 | {{/isNumberType}} 33 | {{#isStringType}} 34 | enum class {{typeName}} { 35 | {{#members}} 36 | @SerializedName({{{value}}}) {{key}}{{^last}},{{/last}} 37 | {{/members}} 38 | } 39 | {{/isStringType}} 40 | {{/enum}} 41 | {{#unionType}} 42 | sealed class {{unionTypeName}}(val value: Any) { 43 | {{#members}} 44 | data class {{capitalizeName}}Value(val value: {{{type}}}) : {{unionTypeName}}() 45 | {{/members}} 46 | } 47 | 48 | class {{unionTypeName}}Adapter : JsonSerializer<{{unionTypeName}}>, JsonDeserializer<{{unionTypeName}}> { 49 | override fun serialize(src: {{unionTypeName}}, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { 50 | return context.serialize(src.value) 51 | } 52 | 53 | override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): {{unionTypeName}} { 54 | {{#members}} 55 | try { 56 | return {{unionTypeName}}.{{capitalizeName}}Value(context.deserialize(json, {{type}}::class.java)) 57 | } catch (e: Exception) { 58 | // Ignore the exception and try the next type 59 | } 60 | {{/members}} 61 | 62 | throw IllegalArgumentException("Unexpected JSON type: ${json.javaClass}") 63 | } 64 | } 65 | {{/unionType}} -------------------------------------------------------------------------------- /example-templates/kotlin-named-types.mustache: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. 3 | * Microsoft Corporation. All rights reserved. 4 | * 5 | * 6 | * This file is automatically generated 7 | * Please DO NOT modify 8 | */ 9 | 10 | package com.microsoft.office.outlook.rooster.web.bridge 11 | 12 | import java.lang.reflect.Type 13 | import com.google.gson.JsonDeserializationContext 14 | import com.google.gson.JsonDeserializer 15 | import com.google.gson.JsonElement 16 | import com.google.gson.JsonPrimitive 17 | import com.google.gson.JsonSerializationContext 18 | import com.google.gson.JsonSerializer 19 | import com.google.gson.annotations.SerializedName 20 | 21 | {{#.}} 22 | 23 | {{> kotlin-named-type}} 24 | {{/.}} 25 | -------------------------------------------------------------------------------- /example-templates/swift-bridge.mustache: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | {{#documentationLines}} 9 | ///{{{.}}} 10 | {{/documentationLines}} 11 | public class {{moduleName}} { 12 | {{#customTags.privateDispatcher}}private {{/customTags.privateDispatcher}}let jsExecutor: BridgeJSExecutor 13 | 14 | init(jsExecutor: BridgeJSExecutor) { 15 | self.jsExecutor = jsExecutor 16 | } 17 | {{#methods}} 18 | 19 | {{#documentationLines}} 20 | ///{{{.}}} 21 | {{/documentationLines}} 22 | public func {{methodName}}({{parametersDeclaration}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}completion: {{#returnType}}@escaping BridgeCompletion<{{returnType}}>{{/returnType}}{{^returnType}}BridgeJSExecutor.Completion? = nil{{/returnType}}) { 23 | {{#parameters.length}} 24 | struct Args: Encodable { 25 | {{#parameters}} 26 | let {{name}}: {{type}} 27 | {{/parameters}} 28 | } 29 | {{/parameters.length}} 30 | {{#parameters.length}} 31 | let args = Args( 32 | {{#parameters}} 33 | {{name}}: {{name}}{{^last}},{{/last}} 34 | {{/parameters}} 35 | ) 36 | {{/parameters.length}} 37 | jsExecutor.execute(with: "{{customTags.invokePath}}", feature: "{{methodName}}", args: {{#parameters.length}}args{{/parameters.length}}{{^parameters}}nil{{/parameters}}, completion: completion) 38 | } 39 | {{/methods}} 40 | } 41 | {{#associatedTypes}} 42 | 43 | {{> swift-named-type}} 44 | {{/associatedTypes}} 45 | -------------------------------------------------------------------------------- /example-templates/swift-named-type.mustache: -------------------------------------------------------------------------------- 1 | {{#custom}} 2 | {{#documentationLines}} 3 | ///{{{.}}} 4 | {{/documentationLines}} 5 | public struct {{typeName}}: Codable { 6 | {{#members}} 7 | {{#documentationLines}} 8 | ///{{{.}}} 9 | {{/documentationLines}} 10 | public var {{name}}: {{type}} 11 | {{/members}} 12 | {{#staticMembers}} 13 | {{#documentationLines}} 14 | ///{{{.}}} 15 | {{/documentationLines}} 16 | private var {{name}}: {{type}} = {{{value}}} 17 | {{/staticMembers}} 18 | 19 | public init({{#members}}{{name}}: {{type}}{{#defaultValue}} = {{defaultValue}}{{/defaultValue}}{{^last}}, {{/last}}{{/members}}) { 20 | {{#members}} 21 | self.{{name}} = {{name}} 22 | {{/members}} 23 | } 24 | } 25 | {{/custom}} 26 | {{#enum}} 27 | {{#documentationLines}} 28 | ///{{{.}}} 29 | {{/documentationLines}} 30 | public enum {{typeName}}: {{valueType}}, Codable { 31 | {{#members}} 32 | {{#documentationLines}} 33 | ///{{{.}}} 34 | {{/documentationLines}} 35 | case {{key}} = {{{value}}} 36 | {{/members}} 37 | } 38 | {{/enum}} 39 | {{#unionType}} 40 | public enum {{unionTypeName}}: Codable { 41 | {{#members}} 42 | case {{uncapitalizeName}}(_ value: {{type}}) 43 | {{/members}} 44 | 45 | public init(from decoder: any Decoder) throws { 46 | let container = try decoder.singleValueContainer() 47 | {{#members}} 48 | {{^last}} 49 | {{^first}}else {{/first}}if let value = try? container.decode({{type}}.self) { 50 | self = .{{uncapitalizeName}}(value) 51 | } 52 | {{/last}} 53 | {{#last}} 54 | else { 55 | let value = try container.decode({{type}}.self) 56 | self = .{{uncapitalizeName}}(value) 57 | } 58 | {{/last}} 59 | {{/members}} 60 | } 61 | 62 | public func encode(to encoder: any Encoder) throws { 63 | var container = encoder.singleValueContainer() 64 | switch self { 65 | {{#members}} 66 | case .{{uncapitalizeName}}(let value): 67 | try container.encode(value) 68 | {{/members}} 69 | } 70 | } 71 | } 72 | {{/unionType}} -------------------------------------------------------------------------------- /example-templates/swift-named-types.mustache: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | // Don't modify this file manually, it's auto generated. 7 | 8 | import UIKit 9 | {{#.}} 10 | 11 | {{> swift-named-type}} 12 | {{/.}} 13 | -------------------------------------------------------------------------------- /example-templates/swift-native-module.mustache: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Microsoft Corporation. All rights reserved. 3 | // 4 | 5 | // swiftformat:disable redundantRawValues 6 | 7 | import Foundation 8 | 9 | public protocol {{moduleName}}: EditorNativeModule { 10 | {{#methods}} 11 | func {{methodName}}({{parametersDeclaration}}{{#returnType}}{{#parametersDeclaration.length}}, {{/parametersDeclaration.length}}completion: @escaping (Result<{{returnType}}, Error>) -> Void{{/returnType}}) 12 | {{/methods}} 13 | } 14 | 15 | public extension {{moduleName}} { 16 | var moduleBridge: NativeModuleBridge { 17 | {{customTags.bridgeName}}(self) 18 | } 19 | } 20 | 21 | class {{customTags.bridgeName}}: NativeModuleBridge { 22 | static let name = "{{customTags.invokePath}}" 23 | 24 | lazy var methodMap: [String: NativeMethod] = [ 25 | {{#methods}} 26 | "{{methodName}}": {{methodName}}, 27 | {{/methods}} 28 | ] 29 | 30 | private let instance: {{moduleName}} 31 | private lazy var decoder = JSONDecoder() 32 | 33 | init(_ instance: {{moduleName}}) { 34 | self.instance = instance 35 | } 36 | {{#methods}} 37 | 38 | private func {{methodName}}(parametersData: Data, completion: @escaping (Result) -> Void) { 39 | {{#parameters.length}} 40 | struct Parameters: Decodable { 41 | {{#parameters}} 42 | var {{name}}: {{type}} 43 | {{/parameters}} 44 | } 45 | 46 | let parameters: Parameters 47 | do { 48 | parameters = try decoder.decode(Parameters.self, from: parametersData) 49 | } 50 | catch { 51 | logAssertFail("Parameters of {{methodName}} are invalid: \(error)") 52 | completion(.failure(NativeMethodError.invalidParameters(parametersData))) 53 | return 54 | } 55 | 56 | {{/parameters.length}} 57 | instance.{{methodName}}{{#parameters.length}}({{#parameters}}{{name}}: parameters.{{name}}{{^last}}, {{/last}}{{/parameters}}){{/parameters.length}}{{#returnType}} { result in 58 | completion(result.toEncodable()) 59 | }{{/returnType}} 60 | } 61 | {{/methods}} 62 | } 63 | {{#associatedTypes}} 64 | 65 | {{> swift-named-type}} 66 | {{/associatedTypes}} 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-gyb", 3 | "version": "0.12.3", 4 | "description": "Generate Native API based on TS interface", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/ts-gyb" 8 | }, 9 | "license": "MIT", 10 | "homepage": "https://github.com/microsoft/ts-gyb#readme", 11 | "keywords": [], 12 | "scripts": { 13 | "build": "tsc", 14 | "clean": "rm -rf dist", 15 | "debug": "ts-node ./src/cli/index", 16 | "start:example:basic": "npm run debug -- --config demo/basic/config.json", 17 | "start:example:mini-editor": "npm run debug -- --config demo/mini-editor/web/config.json", 18 | "test": "mocha -r ts-node/register test/**/*.ts", 19 | "lint": "eslint ./src --ext .js,.ts", 20 | "prettier:write": "prettier --write \"src/**/*.ts\"", 21 | "prettier:check": "prettier --check \"src/**/*.ts\"", 22 | "lint:fix": "npm run lint -- --fix && npm run prettier:write", 23 | "lint:ci": "npm run lint && npm run prettier:check", 24 | "doc:gen:config": "typedoc --out documentation/generated src/cli/configuration.ts --disableSources && rm documentation/generated/modules.md && rm documentation/generated/README.md" 25 | }, 26 | "main": "dist/index.js", 27 | "bin": { 28 | "ts-gyb": "bin/ts-gyb" 29 | }, 30 | "files": [ 31 | "dist/**/*.js", 32 | "dist/**/*.d.ts" 33 | ], 34 | "devDependencies": { 35 | "@types/chai": "^4.2.18", 36 | "@types/mocha": "^9.0.0", 37 | "@types/mustache": "^4.1.1", 38 | "@types/uuid": "^8.3.0", 39 | "@types/yargs": "^15.0.9", 40 | "@typescript-eslint/eslint-plugin": "^4.9.0", 41 | "@typescript-eslint/parser": "^4.9.0", 42 | "chai": "^4.3.4", 43 | "del": "^5.1.0", 44 | "eslint": "^7.5.0", 45 | "eslint-config-airbnb-base": "^14.2.1", 46 | "eslint-config-prettier": "^7.0.0", 47 | "eslint-plugin-import": "^2.22.1", 48 | "mocha": "^9.1.3", 49 | "prettier": "^2.2.1", 50 | "ts-node": "^8.10.2", 51 | "typedoc": "^0.22.11", 52 | "typedoc-plugin-markdown": "^3.10.4", 53 | "uuid": "^8.3.2" 54 | }, 55 | "dependencies": { 56 | "chalk": "^4.1.1", 57 | "glob": "^7.1.6", 58 | "mustache": "^4.2.0", 59 | "typescript": "^4.3.2", 60 | "yargs": "^16.1.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "parser": "babel-ts", 10 | "quoteProps": "as-needed", 11 | "jsxBracketSameLine": false, 12 | } 13 | -------------------------------------------------------------------------------- /src/cli/generate.ts: -------------------------------------------------------------------------------- 1 | import { CodeGenerator, RenderingLanguage } from '../generator/CodeGenerator'; 2 | import { Configuration } from '../configuration'; 3 | import { ParsedTarget } from '../generator/named-types'; 4 | 5 | export function generateWithConfig(config: Configuration): void { 6 | const generator = new CodeGenerator( 7 | new Set(config.parsing.predefinedTypes ?? []), 8 | config.parsing.defaultCustomTags ?? {}, 9 | config.parsing.skipInvalidMethods ?? false, 10 | config.parsing.dropInterfaceIPrefix ?? false 11 | ); 12 | 13 | const namedTargets = Object.fromEntries( 14 | Object.entries(config.parsing.targets).map(([target, targetConfig]) => [ 15 | target, 16 | generator.parseTarget( 17 | targetConfig.source, 18 | targetConfig.exportedInterfaceBases !== undefined ? new Set(targetConfig.exportedInterfaceBases) : undefined, 19 | targetConfig.tsconfigPath 20 | ), 21 | ]) 22 | ); 23 | 24 | let sharedTypes = generator.extractTargetsSharedTypes(Object.values(namedTargets)); 25 | sharedTypes = sharedTypes.concat(Object.values(namedTargets).flatMap((target) => target.sharedTypes)); 26 | generator.printSharedTypes(sharedTypes); 27 | 28 | Object.values(namedTargets).forEach((target) => { 29 | generator.printTarget(target.modules); 30 | }); 31 | 32 | const languageRenderingConfigs = [ 33 | { language: RenderingLanguage.Swift, renderingConfig: config.rendering.swift }, 34 | { language: RenderingLanguage.Kotlin, renderingConfig: config.rendering.kotlin }, 35 | ]; 36 | 37 | languageRenderingConfigs.forEach(({ language, renderingConfig }) => { 38 | if (renderingConfig === undefined) { 39 | return; 40 | } 41 | 42 | renderingConfig.renders.forEach((render) => { 43 | const target = namedTargets[render.target] as ParsedTarget | undefined; 44 | if (target === undefined) { 45 | throw new Error(`target ${render.target} is not defined in the configuration file`); 46 | } 47 | generator.renderModules(target.modules, { 48 | language, 49 | outputPath: render.outputPath, 50 | templatePath: render.template, 51 | typeNameMap: renderingConfig.typeNameMap ?? {}, 52 | }); 53 | }); 54 | 55 | if (sharedTypes.length > 0) { 56 | generator.renderNamedTypes(sharedTypes, { 57 | language, 58 | outputPath: renderingConfig.namedTypesOutputPath, 59 | templatePath: renderingConfig.namedTypesTemplatePath, 60 | typeNameMap: renderingConfig.typeNameMap ?? {}, 61 | }); 62 | } 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import yargs from 'yargs'; 4 | import { Configuration, normalizeConfiguration, RenderConfiguration } from '../configuration'; 5 | import { generateWithConfig } from './generate'; 6 | 7 | const program = yargs(process.argv.slice(2)); 8 | 9 | function run(): void { 10 | /* eslint-disable no-empty-function,@typescript-eslint/no-empty-function */ 11 | program 12 | .scriptName('ts-gyb') 13 | .command(['gen', '*'], 'generate code from a configuration file', () => {}, generate) 14 | .command( 15 | 'list-output', 16 | 'list all output files', 17 | (subprogram) => { 18 | subprogram 19 | .option('language', { description: 'language of the output files to list', choices: ['swift', 'kotlin'] }) 20 | .option('expand', { description: 'expand directories' }); 21 | }, 22 | listOutput 23 | ) 24 | .option('config', { 25 | describe: 'path to the configuration file', 26 | type: 'string', 27 | }) 28 | .demandCommand(1, 'You need at least one command before moving on') 29 | .demandOption('config') 30 | .help() 31 | .parse(); 32 | /* eslint-enable no-empty-function,@typescript-eslint/no-empty-function */ 33 | } 34 | 35 | function generate(args: { config: string }): void { 36 | const config = parseConfig(args.config); 37 | 38 | generateWithConfig(config); 39 | } 40 | 41 | function listOutput(args: { config: string; language?: 'swift' | 'kotlin'; expand: boolean }): void { 42 | const config = parseConfig(args.config); 43 | 44 | let files: string[]; 45 | if (args.language !== undefined) { 46 | const renderingConfig = config.rendering[args.language]; 47 | if (renderingConfig === undefined) { 48 | throw new Error(`Language ${args.language} is not defined in the configuration file`); 49 | } 50 | files = renderingConfig.renders.map((render) => render.outputPath); 51 | } else { 52 | files = Object.values(config.rendering).flatMap((renderingConfig: RenderConfiguration) => 53 | renderingConfig.renders.map((render) => render.outputPath) 54 | ); 55 | } 56 | 57 | files = files.map((file) => path.resolve(file)); 58 | if (args.expand) { 59 | files = files 60 | .map((filePath) => { 61 | if (!fs.lstatSync(filePath).isDirectory()) { 62 | return filePath; 63 | } 64 | 65 | return fs.readdirSync(filePath).map((file) => path.join(filePath, file)); 66 | }) 67 | .flat(); 68 | } 69 | 70 | files = [...new Set(files)]; 71 | console.log(files.join('\n')); 72 | } 73 | 74 | function parseConfig(configPath: string): Configuration { 75 | const configFile = path.resolve(configPath); 76 | const projectDirectory = path.dirname(configFile); 77 | 78 | process.chdir(projectDirectory); 79 | 80 | const rawConfig = JSON.parse(fs.readFileSync(configFile).toString()) as Configuration; 81 | return normalizeConfiguration(rawConfig, projectDirectory); 82 | } 83 | 84 | run(); 85 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { normalizePath } from './utils'; 2 | 3 | export interface TargetParseConfiguration { 4 | /** 5 | * Source file paths. [Glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)) are allowed. 6 | * If it is a relative path, it will be resolved based on the configuration file path. 7 | * 8 | * For example, `["src/api/IEditor.ts", "src/bridge/*.ts"]` 9 | */ 10 | source: string[]; 11 | /** 12 | * Interface names for detecting exported modules. If defined, only interfaces that extends the specified interfaces will be parsed. 13 | * If not defined, interfaces with JSDoc tag `@shouldExport true` would be parsed. 14 | * For example, set it to `["ExportedInterface"]`, all such interfaces would be exported: 15 | * ```ts 16 | * interface SomeInterface extends ExportedInterface {} 17 | * ``` 18 | */ 19 | exportedInterfaceBases?: string[]; 20 | /** 21 | * Provide a specific path for the `tsconfig.json` file. 22 | */ 23 | tsconfigPath?: string; 24 | } 25 | 26 | /** 27 | * Parser configuration 28 | */ 29 | export interface ParseConfiguration { 30 | /** 31 | * Target parse configuration. 32 | */ 33 | targets: Record; 34 | /** 35 | * Names for pre-defined types. 36 | * For example, `CodeGen_Int` for mapping for `number` to integers. 37 | */ 38 | predefinedTypes?: string[]; 39 | /** 40 | * Custom tags for code generation in mustache and its default value. 41 | */ 42 | defaultCustomTags?: Record; 43 | /** 44 | * Drop the `I` prefix for TypeScript interfaces. 45 | * This only works for types used as method parameters or return value. 46 | */ 47 | dropInterfaceIPrefix?: boolean; 48 | /** 49 | * Skip the code generation for invalid methods. If `false`, the code generation will fail when encounter an unsupported type. 50 | */ 51 | skipInvalidMethods?: boolean; 52 | } 53 | 54 | export interface TargetRenderConfiguration { 55 | /** 56 | * Name of the target to be rendered. Targets are defined in `parsing.targets`. 57 | */ 58 | target: string; 59 | /** 60 | * Template file paths. If it is a relative path, it will be resolved based on the configuration file path. 61 | */ 62 | template: string; 63 | /** 64 | * Output directories or paths. If it is a relative path, it will be resolved based on the configuration file path. 65 | * 66 | * For example, `"../ios/AppTarget/Generated"` 67 | */ 68 | outputPath: string; 69 | } 70 | 71 | /** 72 | * Renderer configuration 73 | */ 74 | export interface RenderConfiguration { 75 | /** 76 | * A list of render configurations. 77 | */ 78 | renders: TargetRenderConfiguration[]; 79 | 80 | /** 81 | * Template path for named types. Must be a mustache template. 82 | * If it is a relative path, it will be resolved based on the configuration file path. 83 | * 84 | * For example, `code-templates/named-types.mustache`. 85 | */ 86 | namedTypesTemplatePath: string; 87 | /** 88 | * Output path for named types. 89 | * If it is a relative path, it will be resolved based on the configuration file path. 90 | */ 91 | namedTypesOutputPath: string; 92 | /** 93 | * The mapping from `predefinedTypes` to the existing types in the target language (Kotlin/Swift). 94 | * 95 | * For example, `{ "CodeGen_Int": "Int" }`. 96 | */ 97 | typeNameMap?: Record; 98 | } 99 | 100 | /** 101 | * Language rendering configuration 102 | */ 103 | export interface LanguageRenderingConfiguration { 104 | /** 105 | * Swift renderer configuration 106 | */ 107 | swift?: RenderConfiguration; 108 | /** 109 | * Kotlin renderer configuration 110 | */ 111 | kotlin?: RenderConfiguration; 112 | } 113 | 114 | /** 115 | * Root configuration 116 | */ 117 | export interface Configuration { 118 | /** 119 | * Parser configuration 120 | */ 121 | parsing: ParseConfiguration; 122 | 123 | /** 124 | * Code generation configuration for different languages 125 | */ 126 | rendering: LanguageRenderingConfiguration; 127 | } 128 | 129 | function normalizeRenderConfiguration(basePath: string, config?: RenderConfiguration): RenderConfiguration | undefined { 130 | if (!config) { 131 | return config; 132 | } 133 | let { namedTypesTemplatePath, namedTypesOutputPath } = config; 134 | const { renders, typeNameMap } = config; 135 | 136 | namedTypesOutputPath = normalizePath(namedTypesOutputPath, basePath); 137 | namedTypesTemplatePath = normalizePath(namedTypesTemplatePath, basePath); 138 | 139 | renders.forEach((render) => { 140 | render.template = normalizePath(render.template, basePath); 141 | render.outputPath = normalizePath(render.outputPath, basePath); 142 | }); 143 | 144 | return { 145 | renders, 146 | namedTypesTemplatePath, 147 | namedTypesOutputPath, 148 | typeNameMap, 149 | }; 150 | } 151 | 152 | export function normalizeConfiguration(config: Configuration, basePath: string): Configuration { 153 | const { parsing, rendering } = config; 154 | const { targets, predefinedTypes, defaultCustomTags, dropInterfaceIPrefix, skipInvalidMethods } = parsing; 155 | let { swift: swiftConfig, kotlin: kotlinConfig } = rendering; 156 | 157 | swiftConfig = normalizeRenderConfiguration(basePath, swiftConfig); 158 | kotlinConfig = normalizeRenderConfiguration(basePath, kotlinConfig); 159 | 160 | return { 161 | parsing: { 162 | targets, 163 | predefinedTypes, 164 | defaultCustomTags, 165 | dropInterfaceIPrefix, 166 | skipInvalidMethods, 167 | }, 168 | rendering: { 169 | swift: swiftConfig, 170 | kotlin: kotlinConfig, 171 | }, 172 | }; 173 | } 174 | -------------------------------------------------------------------------------- /src/generator/CodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { 4 | dropIPrefixInCustomTypes, 5 | extractTargetsSharedTypes, 6 | NamedTypeInfo, 7 | ParsedModule, 8 | ParsedTarget, 9 | parseTarget, 10 | } from './named-types'; 11 | import { Parser } from '../parser/Parser'; 12 | import { renderCode } from '../renderer/renderer'; 13 | import { NamedTypeView, ModuleView, InterfaceTypeView, EnumTypeView, UnionTypeView } from '../renderer/views'; 14 | import { serializeModule, serializeNamedType } from '../serializers'; 15 | import { isEnumType, isInterfaceType } from '../types'; 16 | import { applyDefaultCustomTags } from './utils'; 17 | import { ValueTransformer, SwiftValueTransformer, KotlinValueTransformer } from '../renderer/value-transformer'; 18 | 19 | export enum RenderingLanguage { 20 | Swift = 'Swift', 21 | Kotlin = 'Kotlin', 22 | } 23 | 24 | export interface RenderOptions { 25 | language: RenderingLanguage; 26 | outputPath: string; 27 | templatePath: string; 28 | typeNameMap: Record; 29 | } 30 | 31 | export class CodeGenerator { 32 | constructor( 33 | private readonly predefinedTypes: Set, 34 | private readonly defaultCustomTags: Record, 35 | private readonly skipInvalidMethods: boolean, 36 | private readonly dropInterfaceIPrefix: boolean 37 | ) {} 38 | 39 | parseTarget(interfacePaths: string[], exportedInterfaceBases?: Set, tsconfigPath?: string): ParsedTarget { 40 | const parser = new Parser( 41 | interfacePaths, 42 | this.predefinedTypes, 43 | this.skipInvalidMethods, 44 | exportedInterfaceBases, 45 | tsconfigPath 46 | ); 47 | const modules = parser.parse(); 48 | 49 | modules.forEach((module) => applyDefaultCustomTags(module, this.defaultCustomTags)); 50 | 51 | if (this.dropInterfaceIPrefix) { 52 | dropIPrefixInCustomTypes(modules); 53 | } 54 | 55 | return parseTarget(modules); 56 | } 57 | 58 | extractTargetsSharedTypes(targets: ParsedTarget[]): NamedTypeInfo[] { 59 | return extractTargetsSharedTypes(targets); 60 | } 61 | 62 | printTarget(modules: ParsedModule[]): void { 63 | console.log('Modules:\n'); 64 | console.log( 65 | modules 66 | .map((module) => 67 | serializeModule( 68 | module, 69 | module.associatedTypes.map((associatedType) => associatedType.type) 70 | ) 71 | ) 72 | .join('\n\n') 73 | ); 74 | console.log(); 75 | } 76 | 77 | printSharedTypes(sharedTypes: NamedTypeInfo[]): void { 78 | console.log('Shared named types:\n'); 79 | console.log(sharedTypes.map((namedType) => serializeNamedType(namedType.type)).join('\n\n')); 80 | } 81 | 82 | renderModules(modules: ParsedModule[], options: RenderOptions): void { 83 | const valueTransformer = this.getValueTransformer(options.language, options.typeNameMap); 84 | 85 | const moduleViews = modules.map((module) => this.getModuleView(module, valueTransformer)); 86 | 87 | if (path.extname(options.outputPath) === '') { 88 | // The path is a directory 89 | moduleViews.forEach((moduleView) => { 90 | const renderedCode = renderCode(options.templatePath, moduleView); 91 | 92 | this.writeFile( 93 | renderedCode, 94 | path.join(options.outputPath, `${moduleView.moduleName}${this.getFileExtension(options.language)}`) 95 | ); 96 | }); 97 | } else { 98 | moduleViews.forEach((moduleView, index) => { 99 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any 100 | (moduleView as any).last = index === moduleViews.length - 1; 101 | }); 102 | const renderedCode = renderCode(options.templatePath, moduleViews); 103 | this.writeFile(renderedCode, options.outputPath); 104 | } 105 | } 106 | 107 | renderNamedTypes(sharedTypes: NamedTypeInfo[], options: RenderOptions): void { 108 | const valueTransformer = this.getValueTransformer(options.language, options.typeNameMap); 109 | 110 | const namedTypesView = sharedTypes.map((namedType) => this.getNamedTypeView(namedType, valueTransformer)); 111 | const renderedCode = renderCode(options.templatePath, namedTypesView); 112 | this.writeFile(renderedCode, options.outputPath); 113 | } 114 | 115 | private getFileExtension(language: RenderingLanguage): string { 116 | switch (language) { 117 | case RenderingLanguage.Swift: 118 | return '.swift'; 119 | case RenderingLanguage.Kotlin: 120 | return '.kt'; 121 | default: 122 | throw Error('Unhandled language'); 123 | } 124 | } 125 | 126 | private getNamedTypeView(namedType: NamedTypeInfo, valueTransformer: ValueTransformer): NamedTypeView { 127 | let namedTypeView: NamedTypeView; 128 | if (isInterfaceType(namedType.type)) { 129 | namedTypeView = new InterfaceTypeView(namedType.type, namedType.source, valueTransformer); 130 | namedTypeView.custom = true; 131 | } else if (isEnumType(namedType.type)) { 132 | namedTypeView = new EnumTypeView(namedType.type, namedType.source, valueTransformer); 133 | namedTypeView.enum = true; 134 | } else { 135 | namedTypeView = new UnionTypeView(namedType.type, valueTransformer); 136 | namedTypeView.unionType = true; 137 | } 138 | 139 | return namedTypeView; 140 | } 141 | 142 | private getModuleView(module: ParsedModule, valueTransformer: ValueTransformer): ModuleView { 143 | return new ModuleView( 144 | module, 145 | module.associatedTypes.map((associatedType) => this.getNamedTypeView(associatedType, valueTransformer)), 146 | valueTransformer 147 | ); 148 | } 149 | 150 | private getValueTransformer(language: RenderingLanguage, typeNameMap: Record): ValueTransformer { 151 | switch (language) { 152 | case RenderingLanguage.Swift: 153 | return new SwiftValueTransformer(typeNameMap); 154 | case RenderingLanguage.Kotlin: 155 | return new KotlinValueTransformer(typeNameMap); 156 | default: 157 | throw Error('Unhandled language'); 158 | } 159 | } 160 | 161 | private writeFile(content: string, filePath: string): void { 162 | const directory = path.dirname(filePath); 163 | if (!fs.existsSync(directory)) { 164 | fs.mkdirSync(directory, { recursive: true }); 165 | } 166 | 167 | fs.writeFileSync(filePath, content); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/generator/utils.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '../types'; 2 | 3 | export function applyDefaultCustomTags(module: Module, defaultCustomTags: Record): void { 4 | Object.entries(defaultCustomTags).forEach(([key, value]) => { 5 | if (module.customTags[key] !== undefined) { 6 | return; 7 | } 8 | 9 | module.customTags[key] = value; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | 3 | export * from './generator/named-types'; 4 | export * from './generator/CodeGenerator'; 5 | -------------------------------------------------------------------------------- /src/logger/ParserLogger.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import chalk from 'chalk'; 3 | 4 | export function warnMessage(message: string): string { 5 | return chalk.yellowBright(message); 6 | } 7 | 8 | export class ParserLogger { 9 | constructor(private checker: ts.TypeChecker) {} 10 | 11 | warn(message: string): void { 12 | console.warn(warnMessage(message)); 13 | } 14 | 15 | warnSkippedNode(node: ts.Node, reason: string, guide: string): void { 16 | this.warn(`Skipped "${node.getText()}" at ${this.getFileNameAndLine(node)} because ${reason}. ${guide}.`); 17 | } 18 | 19 | private getFileNameAndLine(node: ts.Node): string { 20 | const { line } = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.pos); 21 | 22 | return `${node.getSourceFile().fileName}:${line + 1}`; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/parser/Parser.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { glob } from 'glob'; 3 | import path from 'path'; 4 | import { Module, isInterfaceType } from '../types'; 5 | import { ValueParser } from './ValueParser'; 6 | import { parseTypeJSDocTags } from './utils'; 7 | import { ParserLogger } from '../logger/ParserLogger'; 8 | 9 | export class Parser { 10 | private program: ts.Program; 11 | 12 | private checker: ts.TypeChecker; 13 | 14 | private valueParser: ValueParser; 15 | 16 | constructor( 17 | globPatterns: string[], 18 | predefinedTypes: Set, 19 | skipInvalidMethods = false, 20 | private readonly exportedInterfaceBases: Set | undefined, 21 | tsconfigPath: string | undefined 22 | ) { 23 | const filePaths = globPatterns.flatMap((pattern) => glob.sync(pattern)); 24 | 25 | if (tsconfigPath !== undefined) { 26 | const basePath = path.parse(tsconfigPath).dir; 27 | // eslint-disable-next-line @typescript-eslint/unbound-method 28 | const { config } = ts.readConfigFile(tsconfigPath, ts.sys.readFile); 29 | const { options, projectReferences, errors } = ts.parseJsonConfigFileContent(config, ts.sys, basePath); 30 | this.program = ts.createProgram({ 31 | rootNames: filePaths, 32 | options, 33 | configFileParsingDiagnostics: errors, 34 | projectReferences, 35 | }); 36 | } else { 37 | this.program = ts.createProgram({ 38 | rootNames: filePaths, 39 | options: {}, 40 | }); 41 | } 42 | 43 | this.checker = this.program.getTypeChecker(); 44 | this.valueParser = new ValueParser( 45 | this.checker, 46 | predefinedTypes, 47 | new ParserLogger(this.checker), 48 | skipInvalidMethods 49 | ); 50 | } 51 | 52 | parse(): Module[] { 53 | const modules: Module[] = []; 54 | 55 | this.program.getRootFileNames().forEach((fileName) => { 56 | const sourceFile = this.program.getSourceFile(fileName); 57 | if (sourceFile === undefined) { 58 | throw Error('Source file not found'); 59 | } 60 | ts.forEachChild(sourceFile, (node) => { 61 | const module = this.moduleFromNode(node); 62 | if (module !== null) { 63 | modules.push(module); 64 | } 65 | }); 66 | }); 67 | 68 | return modules; 69 | } 70 | 71 | private moduleFromNode(node: ts.Node): Module | null { 72 | if (!ts.isInterfaceDeclaration(node)) { 73 | return null; 74 | } 75 | 76 | const symbol = this.checker.getSymbolAtLocation(node.name); 77 | if (symbol === undefined) { 78 | throw Error('Invalid module node'); 79 | } 80 | 81 | const exportedInterfaceBases = 82 | node.heritageClauses?.flatMap((heritageClause) => heritageClause.types.map((type) => type.getText())) ?? []; 83 | 84 | const jsDocTagsResult = parseTypeJSDocTags(symbol); 85 | 86 | if (this.exportedInterfaceBases !== undefined) { 87 | if (!exportedInterfaceBases.some((extendedInterface) => this.exportedInterfaceBases?.has(extendedInterface))) { 88 | return null; 89 | } 90 | } else if (!jsDocTagsResult.shouldExport) { 91 | return null; 92 | } 93 | 94 | const result = this.valueParser.parseInterfaceType(node); 95 | if (result && isInterfaceType(result)) { 96 | return { 97 | name: result.name, 98 | members: result.members, 99 | methods: result.methods, 100 | documentation: result.documentation, 101 | exportedInterfaceBases, 102 | customTags: result.customTags, 103 | }; 104 | } 105 | 106 | return null; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/parser/ValueParserError.ts: -------------------------------------------------------------------------------- 1 | export class ValueParserError extends Error { 2 | constructor(public message: string, public guide: string) { 3 | super(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/parser/utils.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | // Defined tags 4 | const SHOULD_EXPORT = 'shouldExport'; 5 | const OVERRIDE_MODULE_NAME = 'overrideModuleName'; 6 | const OVERRIDE_TYPE_NAME = 'overrideTypeName'; 7 | 8 | export function parseTypeJSDocTags(symbol: ts.Symbol): { 9 | shouldExport: boolean; 10 | overrideName: string | null; 11 | customTags: Record; 12 | } { 13 | let shouldExport = false; 14 | let overrideName: string | null = null; 15 | const customTags: Record = {}; 16 | 17 | const tags = symbol.getJsDocTags(); 18 | tags.forEach((tag) => { 19 | const value = ts.displayPartsToString(tag.text); 20 | 21 | if (tag.name === SHOULD_EXPORT) { 22 | shouldExport = value === 'true'; 23 | return; 24 | } 25 | 26 | if (tag.name === OVERRIDE_MODULE_NAME || tag.name === OVERRIDE_TYPE_NAME) { 27 | overrideName = value; 28 | return; 29 | } 30 | 31 | try { 32 | customTags[tag.name] = JSON.parse(value); 33 | } catch { 34 | customTags[tag.name] = value; 35 | } 36 | }); 37 | 38 | return { shouldExport, overrideName, customTags }; 39 | } 40 | 41 | export function isUndefinedOrNull(node: ts.TypeNode): boolean { 42 | if (ts.isLiteralTypeNode(node)) { 43 | return node.literal.kind === ts.SyntaxKind.NullKeyword; 44 | } 45 | if (node.kind === ts.SyntaxKind.UndefinedKeyword) { 46 | return true; 47 | } 48 | if (node.kind === ts.SyntaxKind.NullKeyword) { 49 | return true; 50 | } 51 | return false; 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import Mustache from 'mustache'; 4 | 5 | export function renderCode(templatePath: string, view: View): string { 6 | const template = fs.readFileSync(templatePath).toString(); 7 | const directory = path.dirname(templatePath); 8 | return Mustache.render(template, view, (partialName) => { 9 | const partialPath = path.join(directory, `${partialName}.mustache`); 10 | return fs.readFileSync(partialPath).toString(); 11 | }, { 12 | escape: (value: string) => value, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/utils.ts: -------------------------------------------------------------------------------- 1 | export function getDocumentationLines(documentation: string): string[] { 2 | if (documentation.length === 0) { 3 | return []; 4 | } 5 | 6 | return documentation.split('\n').map((line) => (line.length !== 0 ? ` ${line.trimEnd()}` : '')); 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/value-transformer/KotlinValueTransformer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasicTypeValue, 3 | DictionaryKeyType, 4 | isArraryType, 5 | isBasicType, 6 | isInterfaceType, 7 | isDictionaryType, 8 | isEnumType, 9 | isOptionalType, 10 | isPredefinedType, 11 | ValueType, 12 | Value, 13 | isUnionType, 14 | } from '../../types'; 15 | import { ValueTransformer } from './ValueTransformer'; 16 | 17 | export class KotlinValueTransformer implements ValueTransformer { 18 | constructor(private readonly typeNameMap: Record) {} 19 | 20 | convertValueType(valueType: ValueType): string { 21 | if (isBasicType(valueType)) { 22 | switch (valueType.value) { 23 | case BasicTypeValue.string: 24 | return 'String'; 25 | case BasicTypeValue.number: 26 | return 'Float'; 27 | case BasicTypeValue.boolean: 28 | return 'Boolean'; 29 | default: 30 | throw Error('Type not exists'); 31 | } 32 | } 33 | 34 | if (isInterfaceType(valueType)) { 35 | return this.convertTypeNameFromCustomMap(valueType.name); 36 | } 37 | 38 | if (isEnumType(valueType)) { 39 | return this.convertTypeNameFromCustomMap(valueType.name); 40 | } 41 | 42 | if (isArraryType(valueType)) { 43 | return `Array<${this.convertValueType(valueType.elementType)}>`; 44 | } 45 | 46 | if (isDictionaryType(valueType)) { 47 | let keyType: string; 48 | switch (valueType.keyType) { 49 | case DictionaryKeyType.string: 50 | keyType = 'String'; 51 | break; 52 | case DictionaryKeyType.number: 53 | keyType = 'Int'; 54 | break; 55 | default: 56 | throw Error('Type not exists'); 57 | } 58 | 59 | return `Map<${keyType}, ${this.convertValueType(valueType.valueType)}>`; 60 | } 61 | 62 | if (isOptionalType(valueType)) { 63 | return `${this.convertValueType(valueType.wrappedType)}?`; 64 | } 65 | 66 | if (isPredefinedType(valueType)) { 67 | return this.typeNameMap[valueType.name] ?? valueType.name; 68 | } 69 | 70 | if (isUnionType(valueType)) { 71 | return this.convertTypeNameFromCustomMap(valueType.name); 72 | } 73 | 74 | throw Error('Type not handled'); 75 | } 76 | 77 | convertNonOptionalValueType(valueType: ValueType): string { 78 | if (isOptionalType(valueType)) { 79 | return this.convertValueType(valueType.wrappedType); 80 | } 81 | 82 | return this.convertValueType(valueType); 83 | } 84 | 85 | convertValue(value: Value, type: ValueType): string { 86 | if (isBasicType(type)) { 87 | switch (type.value) { 88 | case BasicTypeValue.boolean: 89 | return (value as boolean) ? 'true' : 'false'; 90 | default: 91 | return JSON.stringify(value); 92 | } 93 | } 94 | 95 | if (isInterfaceType(type)) { 96 | throw Error('Custom type static value is not supported'); 97 | } 98 | 99 | if (isEnumType(type)) { 100 | return `${type.name}.${this.convertEnumKey(value as string)}`; 101 | } 102 | 103 | if (isArraryType(type)) { 104 | return `arrayOf(${(value as Value[]).map((element) => this.convertValue(element, type.elementType)).join(', ')})`; 105 | } 106 | 107 | if (isDictionaryType(type)) { 108 | return `mapOf(${Object.entries(value as Record) 109 | .map(([key, element]) => `${JSON.stringify(key)} to ${this.convertValue(element, type.valueType)}`) 110 | .join(', ')})`; 111 | } 112 | 113 | if (isOptionalType(type)) { 114 | if (value === null) { 115 | return 'null'; 116 | } 117 | return this.convertValue(value, type.wrappedType); 118 | } 119 | 120 | if (isPredefinedType(type)) { 121 | throw Error('Predefined type static value is not supported'); 122 | } 123 | 124 | throw Error('Value not handled'); 125 | } 126 | 127 | convertEnumKey(text: string): string { 128 | let result = text.replace(/\.?([A-Z]+)/g, (_, p1: string) => `_${p1}`); 129 | 130 | const testText = result.replace(/^_/, ''); 131 | if (Number.isNaN(Number(testText))) { 132 | result = testText; 133 | } 134 | 135 | return result.toUpperCase(); 136 | } 137 | 138 | convertTypeNameFromCustomMap(name: string): string { 139 | return this.typeNameMap[name] ?? name; 140 | } 141 | 142 | null(): string { 143 | return 'null'; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/renderer/value-transformer/SwiftValueTransformer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasicTypeValue, 3 | DictionaryKeyType, 4 | isArraryType, 5 | isBasicType, 6 | isInterfaceType, 7 | isDictionaryType, 8 | isEnumType, 9 | isOptionalType, 10 | isPredefinedType, 11 | ValueType, 12 | Value, 13 | isUnionType, 14 | } from '../../types'; 15 | import { ValueTransformer } from './ValueTransformer'; 16 | 17 | export class SwiftValueTransformer implements ValueTransformer { 18 | constructor(private readonly typeNameMap: Record) {} 19 | 20 | convertValueType(valueType: ValueType): string { 21 | if (isBasicType(valueType)) { 22 | switch (valueType.value) { 23 | case BasicTypeValue.string: 24 | return 'String'; 25 | case BasicTypeValue.number: 26 | return 'Double'; 27 | case BasicTypeValue.boolean: 28 | return 'Bool'; 29 | default: 30 | throw Error('Type not exists'); 31 | } 32 | } 33 | 34 | if (isInterfaceType(valueType)) { 35 | return this.convertTypeNameFromCustomMap(valueType.name); 36 | } 37 | 38 | if (isEnumType(valueType)) { 39 | return this.convertTypeNameFromCustomMap(valueType.name); 40 | } 41 | 42 | if (isArraryType(valueType)) { 43 | return `[${this.convertValueType(valueType.elementType)}]`; 44 | } 45 | 46 | if (isDictionaryType(valueType)) { 47 | let keyType: string; 48 | switch (valueType.keyType) { 49 | case DictionaryKeyType.string: 50 | keyType = 'String'; 51 | break; 52 | case DictionaryKeyType.number: 53 | keyType = 'Int'; 54 | break; 55 | default: 56 | throw Error('Type not exists'); 57 | } 58 | 59 | return `[${keyType}: ${this.convertValueType(valueType.valueType)}]`; 60 | } 61 | 62 | if (isOptionalType(valueType)) { 63 | return `${this.convertValueType(valueType.wrappedType)}?`; 64 | } 65 | 66 | if (isPredefinedType(valueType)) { 67 | return this.typeNameMap[valueType.name] ?? valueType.name; 68 | } 69 | 70 | if (isUnionType(valueType)) { 71 | return this.convertTypeNameFromCustomMap(valueType.name); 72 | } 73 | 74 | throw Error('Type not handled'); 75 | } 76 | 77 | convertNonOptionalValueType(valueType: ValueType): string { 78 | if (isOptionalType(valueType)) { 79 | return this.convertValueType(valueType.wrappedType); 80 | } 81 | 82 | return this.convertValueType(valueType); 83 | } 84 | 85 | convertValue(value: Value, type: ValueType): string { 86 | if (isBasicType(type)) { 87 | switch (type.value) { 88 | case BasicTypeValue.boolean: 89 | return (value as boolean) ? 'True' : 'False'; 90 | default: 91 | return JSON.stringify(value); 92 | } 93 | } 94 | 95 | if (isInterfaceType(type)) { 96 | throw Error('Custom type static value is not supported'); 97 | } 98 | 99 | if (isEnumType(type)) { 100 | return `.${this.convertEnumKey(value as string)}`; 101 | } 102 | 103 | if (isArraryType(type)) { 104 | return `[${(value as Value[]).map((element) => this.convertValue(element, type.elementType)).join(', ')}]`; 105 | } 106 | 107 | if (isDictionaryType(type)) { 108 | return `[${Object.entries(value as Record) 109 | .map(([key, element]) => `${JSON.stringify(key)}: ${this.convertValue(element, type.valueType)}`) 110 | .join(', ')}]`; 111 | } 112 | 113 | if (isOptionalType(type)) { 114 | if (value === null) { 115 | return 'nil'; 116 | } 117 | return this.convertValue(value, type.wrappedType); 118 | } 119 | 120 | if (isPredefinedType(type)) { 121 | throw Error('Predefined type static value is not supported'); 122 | } 123 | 124 | throw Error('Value not handled'); 125 | } 126 | 127 | convertEnumKey(text: string): string { 128 | if (text.length === 0) { 129 | return ''; 130 | } 131 | 132 | if (text.toLowerCase() === 'default') { 133 | return '`default`'; 134 | } 135 | 136 | let index = 0; 137 | // Get the index of the first lowercased letter 138 | while (index < text.length) { 139 | if (text[index].toLowerCase() === text[index]) { 140 | break; 141 | } 142 | index += 1; 143 | } 144 | 145 | // Get the index before the first lowercased letter 146 | if (index > 1 && index < text.length && text[index].toLowerCase() === text[index]) { 147 | index -= 1; 148 | } 149 | 150 | return text.slice(0, index).toLowerCase() + text.slice(index); 151 | } 152 | 153 | convertTypeNameFromCustomMap(name: string): string { 154 | return this.typeNameMap[name] ?? name; 155 | } 156 | 157 | null(): string { 158 | return 'nil'; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/renderer/value-transformer/ValueTransformer.ts: -------------------------------------------------------------------------------- 1 | import { ValueType, Value } from '../../types'; 2 | 3 | export interface ValueTransformer { 4 | convertValueType(valueType: ValueType): string; 5 | convertNonOptionalValueType(valueType: ValueType): string; 6 | convertValue(value: Value, type: ValueType): string; 7 | convertEnumKey(text: string): string; 8 | convertTypeNameFromCustomMap(name: string): string; 9 | null(): string; 10 | } 11 | -------------------------------------------------------------------------------- /src/renderer/value-transformer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ValueTransformer'; 2 | export * from './SwiftValueTransformer'; 3 | export * from './KotlinValueTransformer'; 4 | -------------------------------------------------------------------------------- /src/renderer/views/EnumTypeView.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypeSource } from '../../generator/named-types'; 2 | import { EnumSubType, EnumType } from '../../types'; 3 | import { getDocumentationLines } from '../utils'; 4 | import { ValueTransformer } from '../value-transformer'; 5 | 6 | export class EnumTypeView { 7 | constructor( 8 | private readonly enumType: EnumType, 9 | private readonly source: ValueTypeSource, 10 | private readonly valueTransformer: ValueTransformer 11 | ) {} 12 | 13 | get typeName(): string { 14 | return this.valueTransformer.convertTypeNameFromCustomMap(this.enumType.name); 15 | } 16 | 17 | get valueType(): string { 18 | switch (this.enumType.subType) { 19 | case EnumSubType.string: 20 | return 'String'; 21 | case EnumSubType.number: 22 | return 'Int'; 23 | default: 24 | throw Error('Unhandled enum subtype'); 25 | } 26 | } 27 | 28 | get isNumberType(): boolean { 29 | return this.enumType.subType === EnumSubType.number; 30 | } 31 | 32 | get isStringType(): boolean { 33 | return this.enumType.subType === EnumSubType.string; 34 | } 35 | 36 | get members(): { key: string; value: string; documentationLines: string[]; last: boolean }[] { 37 | const { members } = this.enumType; 38 | 39 | return members.map((member, index) => ({ 40 | key: this.valueTransformer.convertEnumKey(member.key), 41 | value: typeof member.value === 'string' ? `"${member.value}"` : `${member.value}`, 42 | documentationLines: getDocumentationLines(member.documentation), 43 | last: index === members.length - 1, 44 | })); 45 | } 46 | 47 | get documentationLines(): string[] { 48 | return getDocumentationLines(this.enumType.documentation); 49 | } 50 | 51 | get customTags(): Record { 52 | return this.enumType.customTags; 53 | } 54 | 55 | get isFromParameter(): boolean { 56 | return (this.source & ValueTypeSource.Parameter) === ValueTypeSource.Parameter; 57 | } 58 | 59 | get isFromReturn(): boolean { 60 | return (this.source & ValueTypeSource.Return) === ValueTypeSource.Return; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/views/InterfaceTypeView.ts: -------------------------------------------------------------------------------- 1 | import { ValueTypeSource } from '../../generator/named-types'; 2 | import { InterfaceType } from '../../types'; 3 | import { getDocumentationLines } from '../utils'; 4 | import { ValueTransformer } from '../value-transformer'; 5 | 6 | export class InterfaceTypeView { 7 | constructor( 8 | private readonly interfaceType: InterfaceType, 9 | private readonly source: ValueTypeSource, 10 | private readonly valueTransformer: ValueTransformer 11 | ) {} 12 | 13 | get typeName(): string { 14 | return this.valueTransformer.convertValueType(this.interfaceType); 15 | } 16 | 17 | get members(): { name: string; type: string; documentationLines: string[]; last: boolean, defaultValue?: string }[] { 18 | const members = this.interfaceType.members.filter((member) => member.staticValue === undefined); 19 | 20 | return members.map((member, index) => ({ 21 | name: member.name, 22 | type: this.valueTransformer.convertValueType(member.type), 23 | documentationLines: getDocumentationLines(member.documentation), 24 | last: index === members.length - 1, 25 | defaultValue: member.defaultValue, 26 | })); 27 | } 28 | 29 | get staticMembers(): { name: string; type: string; value: string; documentationLines: string[] }[] { 30 | return this.interfaceType.members 31 | .filter((member) => member.staticValue !== undefined) 32 | .map((member) => { 33 | if (member.staticValue === undefined) { 34 | throw Error('Value is undefined'); 35 | } 36 | 37 | return { 38 | name: member.name, 39 | type: this.valueTransformer.convertValueType(member.type), 40 | value: this.valueTransformer.convertValue(member.staticValue, member.type), 41 | documentationLines: getDocumentationLines(member.documentation), 42 | }; 43 | }); 44 | } 45 | 46 | get documentationLines(): string[] { 47 | return getDocumentationLines(this.interfaceType.documentation); 48 | } 49 | 50 | get customTags(): Record { 51 | return this.interfaceType.customTags; 52 | } 53 | 54 | get isFromParameter(): boolean { 55 | return (this.source & ValueTypeSource.Parameter) === ValueTypeSource.Parameter; 56 | } 57 | 58 | get isFromReturn(): boolean { 59 | return (this.source & ValueTypeSource.Return) === ValueTypeSource.Return; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/views/MethodView.ts: -------------------------------------------------------------------------------- 1 | import { Method } from '../../types'; 2 | import { getDocumentationLines } from '../utils'; 3 | import { ValueTransformer } from '../value-transformer'; 4 | 5 | export class MethodView { 6 | constructor(private readonly method: Method, private readonly valueTransformer: ValueTransformer) {} 7 | 8 | get methodName(): string { 9 | return this.method.name; 10 | } 11 | 12 | get parametersDeclaration(): string { 13 | return this.parameters.map((parameter) => { 14 | let { defaultValue } = parameter; 15 | if (defaultValue == null) { 16 | return `${parameter.name}: ${parameter.type}`; 17 | } 18 | if (defaultValue === 'null') { 19 | defaultValue = this.valueTransformer.null(); 20 | } 21 | return `${parameter.name}: ${parameter.type} = ${defaultValue}`; 22 | }).join(', '); 23 | } 24 | 25 | get parameters(): { name: string; type: string; defaultValue?: string; last: boolean }[] { 26 | return this.method.parameters.map((parameter, index) => ({ 27 | name: parameter.name, 28 | type: this.valueTransformer.convertValueType(parameter.type), 29 | defaultValue: parameter.defaultValue, 30 | last: index === this.method.parameters.length - 1, 31 | })); 32 | } 33 | 34 | get returnType(): string | null { 35 | if (this.method.returnType === null) { 36 | return null; 37 | } 38 | 39 | return this.valueTransformer.convertValueType(this.method.returnType); 40 | } 41 | 42 | get nonOptionalReturnType(): string | null { 43 | if (this.method.returnType === null) { 44 | return null; 45 | } 46 | 47 | return this.valueTransformer.convertNonOptionalValueType(this.method.returnType); 48 | } 49 | 50 | get documentationLines(): string[] { 51 | return getDocumentationLines(this.method.documentation); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/views/ModuleView.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '../../types'; 2 | import { ValueTransformer } from '../value-transformer/ValueTransformer'; 3 | import { MethodView } from './MethodView'; 4 | import { NamedTypeView } from './index'; 5 | import { getDocumentationLines } from '../utils'; 6 | 7 | export class ModuleView { 8 | constructor( 9 | private readonly module: Module, 10 | readonly associatedTypes: NamedTypeView[], 11 | private readonly valueTransformer: ValueTransformer 12 | ) {} 13 | 14 | get moduleName(): string { 15 | return this.module.name; 16 | } 17 | 18 | get members(): { name: string; type: string; documentationLines: string[]; last: boolean }[] { 19 | const members = this.module.members.filter((member) => member.staticValue === undefined); 20 | 21 | return members.map((member, index) => ({ 22 | name: member.name, 23 | type: this.valueTransformer.convertValueType(member.type), 24 | documentationLines: getDocumentationLines(member.documentation), 25 | last: index === members.length - 1, 26 | })); 27 | } 28 | 29 | get methods(): MethodView[] { 30 | return this.module.methods.map((method) => new MethodView(method, this.valueTransformer)); 31 | } 32 | 33 | get exportedInterfaceBases(): Record { 34 | return Object.fromEntries(this.module.exportedInterfaceBases.map((extendedInterface) => [extendedInterface, true])); 35 | } 36 | 37 | get customTags(): Record { 38 | return this.module.customTags; 39 | } 40 | 41 | get documentationLines(): string[] { 42 | return getDocumentationLines(this.module.documentation); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/views/UnionTypeView.ts: -------------------------------------------------------------------------------- 1 | import { capitalize, uncapitalize } from "../../utils"; 2 | import { BasicType, DictionaryKeyType, DictionaryType, UnionType, ValueType, isArraryType, isBasicType, isDictionaryType } from '../../types'; 3 | import { ValueTransformer } from '../value-transformer'; 4 | 5 | export class UnionTypeView { 6 | constructor( 7 | private readonly value: UnionType, 8 | private readonly valueTransformer: ValueTransformer 9 | ) { } 10 | 11 | get unionTypeName(): string { 12 | return this.valueTransformer.convertTypeNameFromCustomMap(this.value.name); 13 | } 14 | 15 | convertValueTypeToUnionMemberName(valueType: ValueType): string { 16 | if (isArraryType(valueType)) { 17 | return `${this.valueTransformer.convertValueType(valueType.elementType)}Array`; 18 | } 19 | 20 | if (isDictionaryType(valueType)) { 21 | let keyType: string; 22 | switch (valueType.keyType) { 23 | case DictionaryKeyType.string: 24 | keyType = 'String'; 25 | break; 26 | case DictionaryKeyType.number: 27 | keyType = 'Int'; 28 | break; 29 | default: 30 | throw Error('Type not exists'); 31 | } 32 | 33 | return `${this.valueTransformer.convertValueType(valueType.valueType)}For${keyType}Dictionary`; 34 | } 35 | 36 | return this.valueTransformer.convertValueType(valueType); 37 | } 38 | 39 | get members(): { 40 | capitalizeName: string, 41 | uncapitalizeName: string, 42 | type: string; 43 | first: boolean; 44 | last: boolean; 45 | }[] { 46 | const { members } = this.value; 47 | 48 | const dictionaryTypeMembers: DictionaryType[] = []; 49 | let basicTypeMembers: BasicType[] = []; 50 | const otherMembers: ValueType[] = []; 51 | 52 | members.forEach((member) => { 53 | if (isDictionaryType(member)) { 54 | dictionaryTypeMembers.push(member); 55 | } else if (isBasicType(member)) { 56 | basicTypeMembers.push(member); 57 | } else { 58 | otherMembers.push(member); 59 | } 60 | }); 61 | 62 | basicTypeMembers = basicTypeMembers.sort((a, b) => { 63 | // put string to last 64 | if (a.value === 'string' && b.value === 'string') { 65 | return 0; 66 | } 67 | 68 | if (a.value === 'string') { 69 | return 1; 70 | } 71 | 72 | return -1; 73 | }); 74 | 75 | const sortedMembers: ValueType[] = [...otherMembers, ...dictionaryTypeMembers, ...basicTypeMembers]; 76 | return sortedMembers 77 | .map((member, index) => { 78 | const typeName = this.valueTransformer.convertValueType(member); 79 | const memberName = this.convertValueTypeToUnionMemberName(member); 80 | return { 81 | capitalizeName: capitalize(memberName), 82 | uncapitalizeName: uncapitalize(memberName), 83 | type: typeName, 84 | first: index === 0, 85 | last: index === members.length - 1, 86 | }; 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/renderer/views/index.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceTypeView } from './InterfaceTypeView'; 2 | import { EnumTypeView } from './EnumTypeView'; 3 | import { UnionTypeView } from './UnionTypeView'; 4 | 5 | export * from './EnumTypeView'; 6 | export * from './InterfaceTypeView'; 7 | export * from './MethodView'; 8 | export * from './ModuleView'; 9 | export * from './UnionTypeView'; 10 | 11 | export type NamedTypeView = (InterfaceTypeView | EnumTypeView | UnionTypeView) & { custom?: boolean; enum?: boolean; unionType?: boolean; }; 12 | -------------------------------------------------------------------------------- /src/serializers.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { NamedType } from './generator/named-types'; 3 | import { 4 | Field, 5 | isArraryType, 6 | isBasicType, 7 | isInterfaceType, 8 | isDictionaryType, 9 | isEnumType, 10 | isOptionalType, 11 | isPredefinedType, 12 | Method, 13 | Module, 14 | ValueType, 15 | Value, 16 | isUnionType, 17 | } from './types'; 18 | 19 | const keywordColor = chalk.green; 20 | const identifierColor = chalk.blue; 21 | const typeColor = chalk.yellow; 22 | const valueColor = chalk.cyan; 23 | const documentationColor = chalk.gray; 24 | 25 | export function serializeModule(module: Module, associatedTypes: NamedType[]): string { 26 | const serializedAssociatedTypes = associatedTypes.map((associatedType) => serializeNamedType(associatedType)); 27 | const customTags = 28 | Object.keys(module.customTags).length > 0 ? `Custom tags: ${JSON.stringify(module.customTags)}\n` : ''; 29 | 30 | return `${serializeDocumentation(module.documentation)}${documentationColor(customTags)}${keywordColor('Module')} ${ 31 | module.name 32 | } { 33 | ${module.members 34 | .map((member) => `${serializeDocumentation(member.documentation)}${keywordColor('var')} ${serializeField(member)}`) 35 | .join('\n') 36 | .split('\n') 37 | .map((line) => ` ${line}`) 38 | .join('\n')} 39 | 40 | ${module.methods 41 | .map((method) => 42 | serializeMethod(method) 43 | .split('\n') 44 | .map((line) => ` ${line}`) 45 | .join('\n') 46 | ) 47 | .join('\n')}${ 48 | serializedAssociatedTypes.length > 0 49 | ? `\n\n${serializedAssociatedTypes 50 | .join('\n') 51 | .split('\n') 52 | .map((line) => ` ${line}`) 53 | .join('\n')}` 54 | : '' 55 | } 56 | }`; 57 | } 58 | 59 | export function serializeNamedType(namedType: NamedType): string { 60 | const customTags = 61 | Object.keys(namedType.customTags).length > 0 ? `Custom tags: ${JSON.stringify(namedType.customTags)}\n` : ''; 62 | 63 | if (isInterfaceType(namedType)) { 64 | return `${serializeDocumentation(namedType.documentation)}${documentationColor(customTags)}${keywordColor( 65 | 'Type' 66 | )} ${namedType.name} { 67 | ${namedType.members 68 | .map((member) => `${serializeDocumentation(member.documentation)}${keywordColor('var')} ${serializeField(member)}`) 69 | .join('\n') 70 | .split('\n') 71 | .map((line) => ` ${line}`) 72 | .join('\n')} 73 | }`; 74 | } 75 | if (isEnumType(namedType)) { 76 | return `${serializeDocumentation(namedType.documentation)}${documentationColor(customTags)}${keywordColor('Enum')} ${namedType.name 77 | } { 78 | ${namedType.members 79 | .map( 80 | (member) => 81 | `${serializeDocumentation(member.documentation)}${identifierColor(member.key)} = ${valueColor(member.value)}` 82 | ) 83 | .join('\n') 84 | .split('\n') 85 | .map((line) => ` ${line}`) 86 | .join('\n')} 87 | }`; 88 | } 89 | if (isUnionType(namedType)) { 90 | return `${documentationColor(customTags)} 91 | ${namedType.members 92 | .map( 93 | (member) => 94 | serializeValueType(member) 95 | ) 96 | .join(' | ')}`; 97 | } 98 | 99 | throw Error(`Unhandled value type ${JSON.stringify(namedType)}`); 100 | } 101 | 102 | function serializeMethod(method: Method): string { 103 | const serializedReturnType = 104 | method.returnType !== null ? `: ${typeColor(serializeValueType(method.returnType))}` : ''; 105 | return `${serializeDocumentation(method.documentation)}${keywordColor('func')} ${identifierColor( 106 | method.name 107 | )}(${method.parameters 108 | .map((parameter) => `${parameter.name}: ${typeColor(serializeValueType(parameter.type))}`) 109 | .join(', ')})${serializedReturnType}`; 110 | } 111 | 112 | function serializeField(field: Field): string { 113 | const staticValue = 114 | field.staticValue !== undefined ? ` = ${serializeStaticValue(field.staticValue, field.type)}` : ''; 115 | return `${identifierColor(field.name)}: ${typeColor(serializeValueType(field.type))}${staticValue}`; 116 | } 117 | 118 | function serializeValueType(valueType: ValueType): string { 119 | if (isBasicType(valueType)) { 120 | return valueType.value; 121 | } 122 | if (isInterfaceType(valueType)) { 123 | return valueType.name; 124 | } 125 | if (isEnumType(valueType)) { 126 | return valueType.name; 127 | } 128 | if (isArraryType(valueType)) { 129 | return `[${serializeValueType(valueType.elementType)}]`; 130 | } 131 | if (isDictionaryType(valueType)) { 132 | return `[${valueType.keyType}: ${serializeValueType(valueType.valueType)}]`; 133 | } 134 | if (isOptionalType(valueType)) { 135 | return `${serializeValueType(valueType.wrappedType)}?`; 136 | } 137 | if (isPredefinedType(valueType)) { 138 | return valueType.name; 139 | } 140 | 141 | if (isUnionType(valueType)) { 142 | return valueType.name; 143 | } 144 | 145 | throw Error(`Unhandled value type ${JSON.stringify(valueType)}`); 146 | } 147 | 148 | function serializeStaticValue(value: Value, type: ValueType): string { 149 | if (isEnumType(type)) { 150 | return `${type.name}.${value as string}`; 151 | } 152 | 153 | return JSON.stringify(value); 154 | } 155 | 156 | function serializeDocumentation(documentation: string): string { 157 | if (documentation.length === 0) { 158 | return ''; 159 | } 160 | 161 | return documentationColor(`/** 162 | ${documentation 163 | .split('\n') 164 | .map((line) => ` * ${line}`) 165 | .join('\n')} 166 | */ 167 | `); 168 | } 169 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Module { 2 | name: string; 3 | members: Field[]; 4 | methods: Method[]; 5 | documentation: string; 6 | exportedInterfaceBases: string[]; 7 | customTags: Record; 8 | } 9 | 10 | export interface Method { 11 | name: string; 12 | parameters: Field[]; 13 | returnType: ValueType | null; 14 | isAsync: boolean; 15 | documentation: string; 16 | } 17 | 18 | export interface Field { 19 | name: string; 20 | type: ValueType; 21 | staticValue?: Value; 22 | documentation: string; 23 | defaultValue?: string; 24 | } 25 | 26 | export type ValueType = NonEmptyType | OptionalType; 27 | export type NonEmptyType = 28 | | BasicType 29 | | InterfaceType 30 | | TupleType 31 | | EnumType 32 | | ArrayType 33 | | DictionaryType 34 | | PredefinedType 35 | | LiteralType 36 | | UnionType; 37 | 38 | export enum ValueTypeKind { 39 | basicType = 'basicType', 40 | interfaceType = 'interfaceType', 41 | tupleType = 'tupleType', 42 | enumType = 'enumType', 43 | arrayType = 'arrayType', 44 | dictionaryType = 'dictionaryType', 45 | optionalType = 'optionalType', 46 | predefinedType = 'predefinedType', 47 | literalType = 'literalType', 48 | unionType = 'unionType', 49 | } 50 | 51 | interface BaseValueType { 52 | kind: ValueTypeKind; 53 | } 54 | 55 | export enum BasicTypeValue { 56 | string = 'string', 57 | number = 'number', 58 | boolean = 'boolean', 59 | } 60 | 61 | export interface BasicType extends BaseValueType { 62 | kind: ValueTypeKind.basicType; 63 | value: BasicTypeValue; 64 | } 65 | export interface InterfaceType extends BaseValueType, Omit { 66 | kind: ValueTypeKind.interfaceType; 67 | } 68 | 69 | export interface TupleType extends BaseValueType { 70 | kind: ValueTypeKind.tupleType; 71 | members: Field[]; 72 | } 73 | 74 | export enum EnumSubType { 75 | string = 'string', 76 | number = 'number', 77 | } 78 | 79 | export interface EnumField { 80 | key: string; 81 | value: string | number; 82 | documentation: string; 83 | } 84 | 85 | export interface EnumType extends BaseValueType { 86 | kind: ValueTypeKind.enumType; 87 | name: string; 88 | subType: EnumSubType; 89 | members: EnumField[]; 90 | documentation: string; 91 | customTags: Record; 92 | } 93 | 94 | export interface ArrayType extends BaseValueType { 95 | kind: ValueTypeKind.arrayType; 96 | elementType: ValueType; 97 | } 98 | 99 | export enum DictionaryKeyType { 100 | string = 'string', 101 | number = 'number', 102 | } 103 | 104 | export interface DictionaryType extends BaseValueType { 105 | kind: ValueTypeKind.dictionaryType; 106 | keyType: DictionaryKeyType; 107 | valueType: ValueType; 108 | } 109 | 110 | export interface OptionalType extends BaseValueType { 111 | kind: ValueTypeKind.optionalType; 112 | wrappedType: NonEmptyType; 113 | } 114 | 115 | export interface PredefinedType extends BaseValueType { 116 | kind: ValueTypeKind.predefinedType; 117 | name: string; 118 | } 119 | 120 | export type UnionLiteralType = string | number; 121 | 122 | export interface LiteralType extends BaseValueType { 123 | kind: ValueTypeKind.literalType; 124 | memberType: BasicTypeValue.string | BasicTypeValue.number; 125 | members: UnionLiteralType[]; 126 | } 127 | 128 | export interface UnionType extends BaseValueType { 129 | name: string; 130 | kind: ValueTypeKind.unionType; 131 | members: ValueType[]; 132 | customTags: Record; 133 | } 134 | 135 | export function isBasicType(valueType: ValueType): valueType is BasicType { 136 | return valueType.kind === ValueTypeKind.basicType; 137 | } 138 | 139 | export function isInterfaceType(valueType: ValueType): valueType is InterfaceType { 140 | return valueType.kind === ValueTypeKind.interfaceType; 141 | } 142 | 143 | export function isTupleType(valueType: ValueType): valueType is TupleType { 144 | return valueType.kind === ValueTypeKind.tupleType; 145 | } 146 | 147 | export function isEnumType(valueType: ValueType): valueType is EnumType { 148 | return valueType.kind === ValueTypeKind.enumType; 149 | } 150 | 151 | export function isArraryType(valueType: ValueType): valueType is ArrayType { 152 | return valueType.kind === ValueTypeKind.arrayType; 153 | } 154 | 155 | export function isDictionaryType(valueType: ValueType): valueType is DictionaryType { 156 | return valueType.kind === ValueTypeKind.dictionaryType; 157 | } 158 | 159 | export function isOptionalType(valueType: ValueType): valueType is OptionalType { 160 | return valueType.kind === ValueTypeKind.optionalType; 161 | } 162 | 163 | export function isPredefinedType(valueType: ValueType): valueType is PredefinedType { 164 | return valueType.kind === ValueTypeKind.predefinedType; 165 | } 166 | 167 | export function isLiteralType(valueType: ValueType): valueType is LiteralType { 168 | return valueType.kind === ValueTypeKind.literalType; 169 | } 170 | 171 | export function isUnionType(valueType: ValueType): valueType is UnionType { 172 | return valueType.kind === ValueTypeKind.unionType; 173 | } 174 | 175 | // TODO: Define these types to support recursive definition 176 | type BaseValue = string | number | boolean | Record | null; 177 | export type Value = BaseValue | BaseValue[] | Record; 178 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { BasicTypeValue, EnumField, LiteralType } from './types'; 3 | 4 | export function capitalize(text: string): string { 5 | if (text.length === 0) { 6 | return text; 7 | } 8 | 9 | return text[0].toUpperCase() + text.slice(1); 10 | } 11 | 12 | export function uncapitalize(text: string): string { 13 | if (text.length === 0) { 14 | return text; 15 | } 16 | 17 | return text[0].toLowerCase() + text.slice(1); 18 | } 19 | 20 | export function normalizePath(currentPath: string, basePath: string): string { 21 | if (path.isAbsolute(currentPath)) { 22 | return currentPath; 23 | } 24 | const result = path.join(basePath, currentPath); 25 | return result; 26 | } 27 | 28 | export function uniquePathWithMember(ownerName: string, memberName: string): string { 29 | return `${capitalize(ownerName)}Members${capitalize(memberName)}Type`; 30 | } 31 | 32 | export function uniquePathWithMethodParameter(ownerName: string, methodName: string, parameterName: string): string { 33 | return `${capitalize(ownerName)}${capitalize(methodName)}${capitalize(parameterName)}`; 34 | } 35 | 36 | export function uniquePathWithMethodReturnType(ownerName: string, methodName: string): string { 37 | return `${capitalize(ownerName)}${capitalize(methodName)}ReturnType`; 38 | } 39 | 40 | export function basicTypeOfUnion(union: LiteralType): BasicTypeValue { 41 | return union.memberType; 42 | } 43 | 44 | export function membersOfUnion(union: LiteralType): EnumField[] { 45 | const result: EnumField[] = []; 46 | union.members.forEach((value) => { 47 | let key = `${value}`; 48 | if (!Number.isNaN(Number(value))) { 49 | key = `_${key}`; 50 | } 51 | const enumField: EnumField = { 52 | key, 53 | value, 54 | documentation: '', 55 | }; 56 | result.push(enumField); 57 | }); 58 | return result; 59 | } 60 | -------------------------------------------------------------------------------- /test/parser-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | import { expect } from 'chai'; 3 | import { withTempParser, withTempSkipParser } from './utils'; 4 | import { ValueParserError } from '../src/parser/ValueParserError'; 5 | import { BasicTypeValue, ValueTypeKind } from '../src/types'; 6 | 7 | describe('Parser', () => { 8 | it('shouldExport symbol', () => { 9 | const sourceCode = ` 10 | /** 11 | * @shouldExport true 12 | */ 13 | interface ExportTrueInterface {} 14 | 15 | /** 16 | * @shouldExport false 17 | */ 18 | interface ExportFalseInterface {} 19 | 20 | interface NoExportInterface {} 21 | `; 22 | withTempParser(sourceCode, parser => { 23 | const modules = parser.parse(); 24 | expect(modules).to.deep.equal([{ 25 | name: 'ExportTrueInterface', 26 | members:[], 27 | methods: [], 28 | exportedInterfaceBases: [], 29 | documentation: '', 30 | customTags: {} 31 | }]); 32 | }); 33 | }); 34 | 35 | it('Two method syntax', () => { 36 | const sourceCode = ` 37 | /** 38 | * @shouldExport true 39 | */ 40 | interface MockedInterface { 41 | mockedMethod(): void; 42 | mockedFunctionProperty: () => void; 43 | } 44 | `; 45 | withTempParser(sourceCode, parser => { 46 | const modules = parser.parse(); 47 | expect(modules).to.deep.equal([{ 48 | name: 'MockedInterface', 49 | members: [], 50 | methods: [{ 51 | name: 'mockedMethod', 52 | parameters: [], 53 | returnType: null, 54 | isAsync: false, 55 | documentation: '', 56 | }, { 57 | name: 'mockedFunctionProperty', 58 | parameters: [], 59 | returnType: null, 60 | isAsync: false, 61 | documentation: '', 62 | }], 63 | exportedInterfaceBases: [], 64 | documentation: '', 65 | customTags: {}, 66 | }]); 67 | }); 68 | }); 69 | 70 | it('Module and method documentation', () => { 71 | const sourceCode = ` 72 | /** 73 | * This is an example documentation for the module 74 | * @shouldExport true 75 | */ 76 | interface MockedInterface { 77 | /** 78 | * This is an example documentation for the member 79 | */ 80 | mockedMember: string; 81 | /** 82 | * This is an example documentation for the method 83 | */ 84 | mockedMethod(): void; 85 | } 86 | `; 87 | 88 | withTempParser(sourceCode, parser => { 89 | const modules = parser.parse(); 90 | expect(modules).to.deep.equal([{ 91 | name: 'MockedInterface', 92 | members: [{ 93 | name: 'mockedMember', 94 | type: { kind: ValueTypeKind.basicType, value: BasicTypeValue.string }, 95 | documentation: 'This is an example documentation for the member', 96 | }], 97 | methods: [{ 98 | name: 'mockedMethod', 99 | parameters: [], 100 | returnType: null, 101 | isAsync: false, 102 | documentation: 'This is an example documentation for the method', 103 | }], 104 | exportedInterfaceBases: [], 105 | documentation: 'This is an example documentation for the module', 106 | customTags: {}, 107 | }]); 108 | }); 109 | }); 110 | 111 | it('Multiple parameters', () => { 112 | const sourceCode = ` 113 | /** 114 | * @shouldExport true 115 | */ 116 | interface MockedInterface { 117 | multipleParamsMethod(foo: string, bar: number); 118 | } 119 | `; 120 | 121 | withTempParser(sourceCode, parser => { 122 | expect(() => parser.parse()).to.throw(ValueParserError).with.property('message', 'it has multiple parameters'); 123 | }); 124 | }); 125 | 126 | it('Multiple parameters with skip flag', () => { 127 | const sourceCode = ` 128 | /** 129 | * @shouldExport true 130 | */ 131 | interface MockedInterface { 132 | /** 133 | * This documentation should be skipped 134 | */ 135 | multipleParamsMethod(foo: string, bar: number); 136 | /** 137 | * This is an example documentation for the member 138 | */ 139 | mockedMember: string; 140 | /** 141 | * This is an example documentation for the method 142 | */ 143 | mockedMethod(): void; 144 | } 145 | `; 146 | 147 | withTempSkipParser(sourceCode, (parser) => { 148 | const modules = parser.parse(); 149 | expect(modules).to.deep.equal([ 150 | { 151 | name: 'MockedInterface', 152 | exportedInterfaceBases: [], 153 | documentation: '', 154 | customTags: {}, 155 | members: [{ 156 | name: 'mockedMember', 157 | type: { kind: ValueTypeKind.basicType, value: BasicTypeValue.string }, 158 | documentation: 'This is an example documentation for the member', 159 | }], 160 | methods: [{ 161 | name: 'mockedMethod', 162 | parameters: [], 163 | returnType: null, 164 | isAsync: false, 165 | documentation: 'This is an example documentation for the method', 166 | }], 167 | }, 168 | ]); 169 | }, new Set(), true); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import os from 'os'; 3 | import path from 'path'; 4 | import { v4 as UUID } from 'uuid'; 5 | import { Parser } from '../src/parser/Parser'; 6 | import { ValueParserError } from '../src/parser/ValueParserError'; 7 | import { Method, ValueType } from '../src/types'; 8 | 9 | export function withTempSkipParser(sourceCode: string, handler: (parser: Parser) => void, predefinedTypes: Set = new Set(), skipInvalidMethods: boolean = false) { 10 | const tempPath = fs.mkdtempSync(`${os.tmpdir()}/`); 11 | const filePath = path.join(tempPath, `${UUID()}.ts`); 12 | fs.writeFileSync(filePath, sourceCode); 13 | 14 | const parser = new Parser([filePath], predefinedTypes, skipInvalidMethods, undefined, undefined); 15 | handler(parser); 16 | 17 | fs.rmdirSync(tempPath, { recursive: true }); 18 | } 19 | 20 | export function withTempParser(sourceCode: string, handler: (parser: Parser) => void, predefinedTypes: Set = new Set()): void { 21 | withTempSkipParser(sourceCode, handler, predefinedTypes, false) 22 | } 23 | 24 | export function withTempMethodParser(methodCode: string, handler: (parseFunc: () => Method | null) => void, predefinedTypes: Set = new Set(), customTypesCode: string = ''): void { 25 | const sourceCode = ` 26 | ${customTypesCode} 27 | 28 | /** 29 | * @shouldExport true 30 | */ 31 | interface MockedInterface { 32 | ${methodCode} 33 | } 34 | `; 35 | 36 | withTempParser(sourceCode, parser => { 37 | const parseFunc = () => { 38 | const module = parser.parse()[0]; 39 | if (module.methods.length > 1) { 40 | throw Error('Multiple methods found'); 41 | } 42 | 43 | return module.methods.length > 0 ? module.methods[0] : null; 44 | }; 45 | 46 | handler(parseFunc); 47 | }, predefinedTypes); 48 | } 49 | 50 | export function withTempValueParser( 51 | valueTypeCode: string, 52 | handler: (parseFunc: () => { return: ValueType, promiseReturn: ValueType, parameter: ValueType }) => void, 53 | predefinedTypes: Set = new Set(), 54 | customTypesCode: string = ''): void { 55 | const sourceCode = ` 56 | ${customTypesCode} 57 | 58 | /** 59 | * @shouldExport true 60 | */ 61 | interface MockedInterface { 62 | returnTypeMethod(): ${valueTypeCode}; 63 | promiseReturnTypeMethod(): Promise<${valueTypeCode}>; 64 | parameterTypeMethod(args: { foobar: ${valueTypeCode} }): void; 65 | } 66 | `; 67 | 68 | withTempParser(sourceCode, parser => { 69 | const parseFunc = () => { 70 | try { 71 | const module = parser.parse()[0]; 72 | if (module.methods.length !== 3) { 73 | throw Error("The number of parsed methods doesn't match the number of defined methods"); 74 | } 75 | 76 | return { 77 | return: module.methods[0].returnType!, 78 | promiseReturn: module.methods[1].returnType!, 79 | parameter: module.methods[2].parameters[0].type, 80 | } 81 | } catch (error) { 82 | if (error instanceof ValueParserError) { 83 | throw error.message.replace('parameters error: ', '').replace('return type error: ', ''); 84 | } 85 | 86 | throw error 87 | } 88 | }; 89 | 90 | handler(parseFunc); 91 | }, predefinedTypes); 92 | } 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/*.ts", "src/**/*.ts"], 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "target": "es6", 6 | "lib": [ 7 | "es2019" 8 | ], 9 | "module": "commonjs", 10 | "outDir": "dist", 11 | "declaration": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "baseUrl": "" 18 | } 19 | } 20 | --------------------------------------------------------------------------------