├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── controllers.md ├── custom-properties.md ├── customization.md ├── mixin.md ├── parameters.md ├── pipes.md ├── providers.md └── wrapper-properties.md ├── images └── logo.png ├── mocha.opts ├── package-lock.json ├── package.json ├── snippets ├── dart.json └── xml.json ├── src ├── autoclose │ └── autoclose.ts ├── builtin-handlers.ts ├── extension.ts ├── generators │ ├── class-generator.ts │ ├── localization-generator.ts │ └── widget-generator.ts ├── language-features │ ├── dart-ext-types.ts │ ├── providers │ │ ├── dart_completion_item_provider.ts │ │ ├── dart_diagnostic_provider.ts │ │ ├── dart_hover_provider.ts │ │ ├── dart_reference_provider.ts │ │ ├── fix_code_action_provider.ts │ │ └── ranking_code_action_provider.ts │ ├── utils.ts │ └── xmlUtils.ts ├── manager.ts ├── models │ ├── config.ts │ └── models.ts ├── parser │ ├── parser.ts │ ├── syntax.ts │ └── types.ts ├── property-handlers │ ├── builder.test.ts │ ├── builder.ts │ ├── child-builder.test.ts │ ├── child-builder.ts │ ├── child-wrapper-property.test.ts │ ├── child-wrapper-property.ts │ ├── form-control.ts │ ├── form-group.ts │ ├── form-submit.ts │ ├── form.test.ts │ ├── if-element.test.ts │ ├── if-element.ts │ ├── if.test.ts │ ├── if.ts │ ├── item-builder-property.ts │ ├── item-builder.test.ts │ ├── item-builder.ts │ ├── repeat.test.ts │ ├── repeat.ts │ ├── switch-case.ts │ ├── switch.test.ts │ ├── switch.ts │ ├── wrapper-animation.test.ts │ ├── wrapper-animation.ts │ ├── wrapper-consumer-property.test.ts │ ├── wrapper-consumer-property.ts │ ├── wrapper-disable-property.test.ts │ ├── wrapper-disable-property.ts │ ├── wrapper-property.test.ts │ ├── wrapper-property.ts │ ├── wrapper-stream-property.test.ts │ └── wrapper-stream-property.ts ├── providers │ ├── property-handler-provider.ts │ └── value-transformers-provider.ts ├── resolvers │ ├── pipe-value-resolver.ts │ ├── pipes.test.ts │ ├── property-element.test.ts │ ├── property-resolver.ts │ ├── use.test.ts │ ├── util.ts │ └── widget-resolver.ts ├── test │ ├── index.ts │ └── shared.ts ├── utils.ts └── value-transformers │ ├── color.ts │ ├── decoration.ts │ ├── edge-insets.ts │ └── enum.ts ├── tsconfig.json ├── tslint.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | *.txt 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.vscode-typescript-tslint-plugin" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Run Extension", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: watch" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "runtimeExecutable": "${execPath}", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test" 28 | ], 29 | "outFiles": [ 30 | "${workspaceFolder}/out/test/**/*.js" 31 | ], 32 | "preLaunchTask": "npm: watch" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "mochaExplorer.files": "**/*.test.ts", 12 | "mochaExplorer.require": "ts-node/register", 13 | "mochaExplorer.optsFile": "mocha.opts" 14 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/tslint.json 9 | **/*.map 10 | **/*.ts 11 | .vscode 12 | node_modules 13 | out/ 14 | src/ 15 | tsconfig.json 16 | webpack.config.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.0.27] - 2020-10-4 3 | Breaking changes: 4 | - rename the custom property `` to ``. 5 | 6 | ## [0.0.25] - 2020-06-18 7 | - Add item data type for builder, itemBuilder, childBuilder and repeat. 8 | 9 | ## [0.0.18] - 2020-06-18 10 | New features: 11 | - `` now has anew `superParamName` attribute which will pass the parameter to super class constructor. 12 | Breaking changes: 13 | - `` will now return a null (instead of Container(width: 0, height: 0)) in the else statement, if there is no `` provided. 14 | Fixes: 15 | - Fix goto definition bug in the new vscode release. 16 | 17 | ## [0.0.10] - 2019-08-15 18 | Breaking changes: 19 | - :formControl now accept both a variable and a string value, so current usge will break and all you need to do is to convert this :formControl="MyControlName" to :formControl="'MyControlName'". 20 | 21 | ## [0.0.8] - 2019-07-27 22 | - Added language features: 23 | - Code completion 24 | - Hover information 25 | - Go to definition 26 | 27 | ## [0.0.4] - 2019-07-17 28 | - Update :submitForm 29 | 30 | ## [0.0.1] - 2019-07-16 31 | - Initial release 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 WaseemDev 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Imagine that you can do this : 3 | ```XML 4 | 11 | ``` 12 | Instead of this: 13 | ```dart 14 | final size = MediaQuery.of(context).size; 15 | final __widget = StreamBuilder( 16 | initialData: ctrl.textVisible.value, 17 | stream: ctrl.textVisible, 18 | builder: (BuildContext context, snapshot) { 19 | if (snapshot.data) { 20 | return Opacity( 21 | opacity: .9, 22 | child: Center( 23 | child: Container( 24 | color: Colors.blue, 25 | height: (size.height * 50) / 100.0, 26 | width: (size.width * 50) / 100.0, 27 | child: Text( 28 | 'Hello world!' 29 | ) 30 | ) 31 | ) 32 | ); 33 | } 34 | else { 35 | return Container(width: 0, height: 0); 36 | } 37 | } 38 | ); 39 | return __widget; 40 | ``` 41 | Which is about 20 lines of code, and if you just updated the `:text` property to use a stream variable `:text="ctrl.myTextStream | stream"` that will add another 4 lines of code for the StreamBuilder. 42 | 43 | 44 | Extension features: 45 | -------- 46 | * Separates UI code (widget and widget's state) from the business logic. 47 | * Brings some Angular's features like pipes, conditionals... 48 | * Provides built-in properties & pipes to make the coding much easier. 49 | * Generates localization code depending on json files. 50 | * Forms & animation made easy. 51 | * Customizable! so developers can add their own properties and modify some features. 52 | * Supports Code completion, hover information, Go to Definition, diagnostics and code actions. 53 | 54 | 55 | ## Example 56 | [Here is a working example](https://github.com/waseemdev/flutter_xmllayout_example) 57 | 58 | 59 | # Get Started 60 | 61 | 1. Install the extension from [vscode marketplace](https://marketplace.visualstudio.com/items?itemName=WaseemDev.flutter-xml-layout) 62 | 2. Create a new flutter project 63 | 3. Install prerequisites packages: 64 | * [flutter_xmllayout_helpers](https://pub.dartlang.org/packages/flutter_xmllayout_helpers) 65 | * [provider](https://pub.dartlang.org/packages/provider) 66 | * flutter_localizations 67 | ```yaml 68 | dependencies: 69 | flutter: 70 | sdk: flutter 71 | flutter_localizations: 72 | sdk: flutter 73 | provider: ^3.0.0+1 74 | flutter_xmllayout_helpers: ^0.0.9 75 | ``` 76 | 4. Apply one of the following steps: 77 | * Clear all `main.dart` content then use `fxml_app` snippet to create the app. 78 | * Modify `main.dart` to use `MultiProvider` from `provider` package: 79 | - Register `PipeProvider` (from `flutter_xmllayout_helpers` package) as a provider. 80 | - Register `RouteObserver` as a provider (only if you want to use RouteAware events in your widgets' controllers). 81 | 82 | ## Localization: 83 | 1. Create `i18n` folder inside `lib` folder and add JSON files named with locale codes e.g. `en.json`. 84 | 2. Import `i18n/gen/delegate.dart` in the main file. 85 | 3. Register `AppLocalizationsDelegate()` in `localizationsDelegates` parameter of the `MaterialApp`. 86 | 4. To use localized text in the UI see [Pipes](./docs/pipes.md) docs. 87 | 88 | ## XML layout: 89 | 1. Create a new folder and name it as your page/widget name e.g. `home`. 90 | 2. Then create home.xml file inside `home` folder. 91 | 3. Use `fxml_widget` snippet to create the starter layout, modify it as you want then save it. the extension will generate a file named `home.xml.dart` which contains UI code, and `home.ctrl.dart` file (if not exists) that contains the controller class which is the place you should put your code in (will be generated only if you added `controller` property). 92 | 93 | Example: 94 | ```XML 95 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | <Text text="'Home'" /> 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ``` 118 | 119 | `HomePage` (root element) the name of your widget. 120 | `controller` an optional property, the controller name you want to generate. 121 | `routeAware` an optional property, which generates navigation events (`didPush()`, `didPop()`, `didPushNext()` and `didPopNext()`). 122 | `xmlns:*` an optional property(s) used to import packges and files to be used in HomePage class. (in this example we imported cupertino.dart to use CupertinoIcons). 123 | 124 | 125 | ## Controller: 126 | If you added a `controller` property to your widget then will be generated (if not exists), the file looks like this: 127 | ```dart 128 | import 'package:flutter/widgets.dart'; 129 | import 'home.xml.dart'; 130 | 131 | class HomeController extends HomeControllerBase { 132 | 133 | // 134 | // here you can add you own logic and call the variables and methods 135 | // within the XML file. e.g. 136 | // 137 | 138 | @override 139 | void didLoad(BuildContext context) { 140 | } 141 | 142 | @override 143 | void onBuild(BuildContext context) { 144 | } 145 | 146 | @override 147 | void afterFirstBuild(BuildContext context) { 148 | } 149 | 150 | @override 151 | void dispose() { 152 | super.dispose(); 153 | } 154 | } 155 | ``` 156 | 157 | # Features documentation 158 | 159 | ### 1. [Wrapper properties](./docs/wrapper-properties.md) 160 | ### 2. [Pipes](./docs/pipes.md) 161 | ### 3. [Custom properties](./docs/custom-properties.md) 162 | ### 4. [Injecting providers](./docs/providers.md) 163 | ### 5. [Parameters](./docs/parameters.md) 164 | ### 6. [Adding controllers to widgets](./docs/controllers.md) 165 | ### 7. [Adding mixin to widget's states](./docs/mixins.md) 166 | ### 8. [Localization](./docs/localization.md) 167 | ### 9. [Developer customization](./docs/customization.md) 168 | 169 | -------------------------------------------------------------------------------- /docs/controllers.md: -------------------------------------------------------------------------------- 1 | 2 | # Adding controllers to widgets 3 | The controllers could be defined with more than way and be accessed directly from the controller class or from the XML file. 4 | 5 | The simple way is by specifying the type and the name, and the extension will instantiate it for you: 6 | ```XML 7 | 8 | ``` 9 | Or 10 | ```XML 11 | 12 | ``` 13 | 14 | Another way is to define it within the controller class: 15 | ```dart 16 | final myTextEditingController = new TextEditingController(); 17 | ``` 18 | And you can access it in XML: 19 | ```XML 20 | 21 | ``` 22 | 23 | Sometimes you might need to access the instance of the State, for example, For AnimationControler to work properly, the current State (which inherets from `SingleTickerProviderStateMixin` [mixin](./mixin.md)) must be passed as parameter: `AnimationController(vsync: this)`. for this case you can define it as a `` in the XML file as follow: 24 | ```XML 25 | 26 | 27 | 28 | 29 | 30 | ... 31 | 32 | ``` 33 | 34 | ***NOTE*** The controllers defined using `` can be accessed within the controller **after** `didLoad()`, not in the constructor: 35 | ```dart 36 | @override 37 | void didLoad() { 38 | animationController.forward(); 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/mixin.md: -------------------------------------------------------------------------------- 1 | 2 | # Adding mixin to widget's states 3 | In the previous example we created [AnimationController](./controllers.md) and passed `this` (which is the State with `SingleTickerProviderStateMixin` mixin) to the controller, but how to add this mixin to the widget? 4 | This is done through the `` tag: 5 | ```XML 6 | 7 | 8 | ... 9 | 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/parameters.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Passing parameters to widgets 4 | Parameters are used to pass data from one widget to another, and since you can't modify the widget code (generated file), the XML structure enables you to declare the parameter(s) as follow: 5 | ```XML 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | ... 35 | 36 | ``` 37 | 38 | And you can access it directly in the XML file: 39 | ```XML 40 | 41 | ``` 42 | 43 | Or in the controller class **after** `didLoad()`, not in the constructor: 44 | ```dart 45 | @override 46 | void didLoad() { 47 | print(category.name); 48 | } 49 | ``` 50 | 51 | Then, from another widget, pass parameter's value as usual: 52 | ```xml 53 | 54 | 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/pipes.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Pipes 4 | Pipes feature enables you to separate value conversion/transformation from the UI code in a convenient way. 5 | For example, one of the most useful pipe is `translate`: 6 | ```XML 7 | 8 | ``` 9 | Result: 10 | ```dart 11 | Text(_pipeProvider.transform(context, "translate", ctrl.greatingText, [])) 12 | ``` 13 | 14 | To create the `translate` pipe extend the `Pipe` class and override the `transform` & `name`: 15 | ```dart 16 | class MyTranslatePipe extends Pipe { 17 | String get name => 'translate'; // unique name 18 | 19 | dynamic transform(BuildContext context, value, args) { 20 | // write your own logic... 21 | 22 | // AppLocalizations class generated by this extension if you have JSON files inside lib/i18n folder. 23 | return AppLocalizations.of(context).getTranslation(value) ?? ''; 24 | } 25 | } 26 | ``` 27 | 28 | Then register your pipe in the `main` file: 29 | ```dart 30 | 31 | PipeProvider pipeProvider = new PipeProvider(); 32 | pipeProvider.register(new MyTranslatePipe()); 33 | 34 | runApp( 35 | MultiProvider( 36 | providers: [ 37 | Provider(builder: (_) => pipeProvider) 38 | ], 39 | ... 40 | ) 41 | ) 42 | ``` 43 | 44 | 45 | ## Built-in pipes 46 | 47 | ### 1. stream 48 | ```XML 49 | 50 | ``` 51 | Result: 52 | ```dart 53 | StreamBuilder( 54 | initialData: null, 55 | stream: ctrl.greatingTextStream, 56 | builder: (BuildContext context, textSnapshot) { 57 | final textValue = textSnapshot.data; 58 | if (textValue == null) { 59 | return Container(height: 0, width: 0); 60 | } 61 | return Text( 62 | textValue 63 | ); 64 | } 65 | ); 66 | ``` 67 | 68 | If you want to add initialData you can pass it as parameter: 69 | ```XML 70 | 71 | ``` 72 | Result: 73 | ```dart 74 | StreamBuilder( 75 | initialData: 'Please wait...', 76 | stream: ctrl.greatingTextStream, 77 | builder: (BuildContext context, textSnapshot) { 78 | final textValue = textSnapshot.data; 79 | if (textValue == null) { 80 | return Container(height: 0, width: 0); 81 | } 82 | return Text( 83 | textValue 84 | ); 85 | } 86 | ); 87 | ``` 88 | 89 | Or if you have a `BehaviorSubject` you can pass its value as initialData parameter: 90 | ```XML 91 | 92 | ``` 93 | 94 | ### 2. behavior 95 | 96 | This is the same as `stream` but used with `BehaviorSubject` and add its value to the initialValue of `StreamBuilder`, to simplify last example can be written: 97 | ```XML 98 | 99 | ``` 100 | 101 | ### 3. future 102 | ```XML 103 | 104 | ``` 105 | Result: 106 | ```dart 107 | FutureBuilder( 108 | future: ctrl.greatingTextFuture, 109 | builder: (BuildContext context, textSnapshot) { 110 | final textValue = textSnapshot.data; 111 | if (textValue == null) { 112 | return Container(height: 0, width: 0); 113 | } 114 | return Text( 115 | textValue 116 | ); 117 | } 118 | ); 119 | ``` 120 | 121 | 122 | ***NOTE*** You can't use `stream`, `behavior` or `future` pipe for a property of non-widget element (e.g. InputDecoration), because `StreamBuilder` and `FutureBuilder` can only returns of Widget type. 123 | 124 | 125 | ### 4. widthPercent & heightPercent 126 | 127 | ```XML 128 | 129 | ``` 130 | 131 | ----------- 132 | You can use multiple pipes with (braces): 133 | ```XML 134 | 135 | 136 | 137 | ``` 138 | 139 | You also can use pipes chaining: 140 | ```XML 141 | 142 | ``` 143 | 144 | ***NOTE*** You can't use multi-chained stream or future e.g. `text="streamThatReturnsStream | stream | stream"` or `text="textStream | stream | anotherPipe | stream"`. only one stream/future per chain, but you can, of course, use grouped pipes and each group has one stream or future. 145 | 146 | -------------------------------------------------------------------------------- /docs/providers.md: -------------------------------------------------------------------------------- 1 | 2 | # Injecting providers 3 | You can inject any provider/service you want in the XML file: 4 | ```XML 5 | 6 | 7 | ... 8 | 9 | ``` 10 | 11 | Then can be accessed directly in the same XML file, or in the controller class **after** `didLoad()`, not in the constructor. 12 | ```dart 13 | @override 14 | void didLoad() { 15 | dataService.doSomething(); 16 | } 17 | ``` 18 | 19 | And don't forget to add the provider/service to your providers: 20 | ```dart 21 | runApp( 22 | MultiProvider( 23 | providers: [ 24 | Provider(builder: (_) => DataService()) 25 | ], 26 | ... 27 | ) 28 | ) 29 | ``` 30 | 31 | ***Note*** If the type of the provider is `ChangeNotifierProvider` the whole widget will rebuild when the provider changes, and that is by design, so if you don't want to rebuild the whole widget try using [:consumer](./wrapper-properties.md) for this case. 32 | -------------------------------------------------------------------------------- /docs/wrapper-properties.md: -------------------------------------------------------------------------------- 1 | 2 | # Wrapper properties 3 | Wrapper properties are an easy way to wrap any widget with another one, so instead of writting this: 4 | ```XML 5 | 6 | 7 | 8 | 9 | 10 | ``` 11 | You write this: 12 | ```XML 13 | 14 | 15 | 16 | ``` 17 | Or even this: 18 | ```XML 19 | 20 | ``` 21 | 22 | All wrapper properties start with `:` to avoid ambiguity with the real properties which might have the same name so you can write the both of previous examples. 23 | 24 | ### 1. :margin 25 | 26 | ```XML 27 | 28 | 29 | 30 | 31 | ``` 32 | Result: 33 | ```dart 34 | Padding( 35 | padding: const EdgeInsets.all(4), 36 | child: Text('Hello!') 37 | ) 38 | Padding( 39 | padding: const symmetric(vertical: 1, horizontal: 2), 40 | child: Text('Hello!') 41 | ) 42 | Padding( 43 | padding: const EdgeInsets.fromLTRB(2, 1, 2, 3), 44 | child: Text('Hello!') 45 | ) 46 | Padding( 47 | padding: const EdgeInsets.fromLTRB(4, 1, 2, 3), 48 | child: Text('Hello!') 49 | ) 50 | ``` 51 | The value formatting follows Web standards (top right bottom left). 52 | 53 | ### 2. :opacity 54 | 55 | ```XML 56 | 57 | ``` 58 | Result: 59 | ```dart 60 | Opacity( 61 | opacity: 0.5, 62 | child: Text('Hello!') 63 | ) 64 | ``` 65 | 66 | ### 3. :visible 67 | 68 | ```XML 69 | 70 | ``` 71 | Result: 72 | ```dart 73 | Visibility( 74 | visible: false, 75 | child: Text('Hello world!') 76 | ) 77 | ``` 78 | 79 | ### 4. :padding 80 | 81 | This is actualy a child-wrapper that wrap the child widget: 82 | ```XML 83 | 84 | 85 | 86 | ``` 87 | Result: 88 | ```dart 89 | Card( 90 | child: Padding( 91 | padding: const EdgeInsets.all(4), 92 | child: Text( 93 | 'Hello!' 94 | ) 95 | ) 96 | ); 97 | ``` 98 | The target widget must have a `child` property. and for this reason the `Text` can't have a `:padding` but can have a `:margin`. 99 | 100 | 101 | ### 5. :text 102 | 103 | This also behaves as a child-wrapper but doesn't wrap the child, instead it adds a `Text` child to the target widget: 104 | ```XML 105 | 106 | ``` 107 | Result: 108 | ```dart 109 | RaisedButton( 110 | child: Text( 111 | 'Hello!' 112 | ) 113 | ); 114 | ``` 115 | 116 | ### 6. :icon 117 | 118 | Its like `:text` but for icons: 119 | ```XML 120 | 121 | ``` 122 | Result: 123 | ```dart 124 | RaisedButton( 125 | child: Icon( 126 | Icons.home 127 | ) 128 | ); 129 | ``` 130 | 131 | ### 7. :width & :height 132 | 133 | Adding one or both of these properties will wrap the target widget with SizedBox: 134 | ```XML 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | ``` 145 | Result: 146 | ```dart 147 | SizedBox( 148 | width: 100, 149 | child: Card( 150 | child: Text( 151 | 'Width only' 152 | ) 153 | ) 154 | ), 155 | SizedBox( 156 | height: 200, 157 | child: Card( 158 | child: Text( 159 | 'Height only' 160 | ) 161 | ) 162 | ), 163 | SizedBox( 164 | height: 200, 165 | width: 100, 166 | child: Card( 167 | child: Text( 168 | 'Width and height' 169 | ) 170 | ) 171 | ) 172 | ``` 173 | 174 | ### 8. :onTap & :onDoubleTap & :onLongPress 175 | 176 | Wraps the target widget with `GestureDetector` and maps each event to its function: 177 | ```XML 178 | 179 | ``` 180 | Result: 181 | ```dart 182 | GestureDetector( 183 | onTap: ctrl.doSomthing, 184 | child: Column( 185 | children: [ 186 | ] 187 | ) 188 | ) 189 | ``` 190 | 191 | ### 9. :theme 192 | 193 | Wraps the target widget with `Theme`: 194 | ```XML 195 | 196 | ``` 197 | Result: 198 | ```dart 199 | Theme( 200 | data: ctrl.myTheme, 201 | child: Text( 202 | 'Hello' 203 | ) 204 | ) 205 | ``` 206 | 207 | Or you could create a custom [pipe](./pipes.md) which provides the themes: 208 | ```XML 209 | 210 | ``` 211 | Result: 212 | ```dart 213 | Theme( 214 | data: _pipeProvider.transform(context, 'customTheme', 'my_theme_name', []), 215 | child: Text( 216 | 'Hello' 217 | ) 218 | ) 219 | ``` 220 | 221 | 222 | ### 10. :hero 223 | 224 | Wraps the target widget with `Hero`: 225 | 226 | ```XML 227 | 228 | ``` 229 | Result: 230 | ```dart 231 | Hero( 232 | tag: 'image-cover', 233 | child: Image.asset( 234 | 'assets/images/image_name.png' 235 | ) 236 | ) 237 | ``` 238 | 239 | 240 | ### 11. :aspectRatio 241 | 242 | Wraps the target widget with `AspectRatio`: 243 | ```XML 244 | 245 | ``` 246 | Result: 247 | ```dart 248 | AspectRatio( 249 | aspectRatio: 16.0/9.0, 250 | child: Image.asset( 251 | 'assets/images/image_name.png' 252 | ) 253 | ) 254 | ``` 255 | 256 | 257 | ### 12. :center 258 | 259 | Wraps the target widget with `Center`: 260 | 261 | ```XML 262 | 263 | ``` 264 | Result: 265 | ```dart 266 | Center( 267 | child: Text( 268 | 'Hello' 269 | ) 270 | ) 271 | ``` 272 | 273 | 274 | ### 13. :align 275 | 276 | Wraps the target widget with `Align`: 277 | 278 | ```XML 279 | 280 | ``` 281 | Result: 282 | ```dart 283 | Align( 284 | alignment: Alignment.center, 285 | child: Text( 286 | 'Hello' 287 | ) 288 | ) 289 | ``` 290 | 291 | 292 | ### 14. :flex 293 | 294 | Wraps the target widget with `Expanded`: 295 | ```XML 296 | 297 | ``` 298 | Result: 299 | ```dart 300 | Expanded( 301 | flex: 2, 302 | child: Text( 303 | 'Hello' 304 | ) 305 | ) 306 | ``` 307 | 308 | 309 | ### 15. :disable 310 | 311 | Wraps any widget has an event handler (e.g.: onPressed, onChange), the target widget will be disabled according to the `:disable`'s value. 312 | ```XML 313 | 314 | 315 | 316 | ``` 317 | Result: 318 | ```dart 319 | StreamBuilder( 320 | initialData: true, 321 | stream: statusStream, 322 | builder: (BuildContext context, statusStreamSnapshot) { 323 | final statusStreamValue = statusStreamSnapshot.data; 324 | return Disable( 325 | event: ctrl.login, 326 | value: statusStreamValue, 327 | builder: (BuildContext context, event) { 328 | return RaisedButton( 329 | onPressed: event, 330 | child: Text( 331 | 'Login' 332 | ) 333 | ); 334 | } 335 | ); 336 | } 337 | ) 338 | ``` 339 | 340 | 341 | ### 16. :consumer 342 | 343 | `:consumer` wraps the target widget with a `Consumer` widget (which is part of `provider` package), this can be used instead of injecting the [provider](./providers.md) at the top level of the current Widget's State to avoid rebuilding the whole widget when the provider changes. 344 | ```XML 345 | 346 | 347 | 348 | ``` 349 | Result: 350 | ```dart 351 | Consumer( 352 | builder: (BuildContext context, MyProvider myProvider, Widget child) { 353 | return MyWidget( 354 | title: myProvider.title, 355 | child: Text( 356 | myProvider.greatingText 357 | ) 358 | ); 359 | } 360 | ) 361 | ``` 362 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waseemdev/vscode-flutter.xml-layout/c5dc81fd13720620b49b4a04de3e70893870578b/images/logo.png -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | 2 | -- require ts-node/register 3 | --ui tdd 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-xml-layout", 3 | "displayName": "XML Layout for Flutter", 4 | "description": "XML Layout for Flutter. Brings Angular's style to Flutter!", 5 | "publisher": "WaseemDev", 6 | "version": "0.0.33", 7 | "icon": "images/logo.png", 8 | "repository": { 9 | "type": "github", 10 | "url": "https://github.com/waseemdev/vscode-flutter.xml-layout" 11 | }, 12 | "engines": { 13 | "vscode": "^1.34.0" 14 | }, 15 | "keywords": [ 16 | "flutter", 17 | "dart", 18 | "widgets", 19 | "xml", 20 | "angular" 21 | ], 22 | "categories": [ 23 | "Other" 24 | ], 25 | "activationEvents": [ 26 | "*" 27 | ], 28 | "main": "./dist/extension.js", 29 | "contributes": { 30 | "commands": [ 31 | { 32 | "command": "flutter.xml-layout.regenerate-all", 33 | "title": "XML Layout for Flutter: Re-generate all XML & JSON files" 34 | } 35 | ], 36 | "snippets": [ 37 | { 38 | "language": "dart", 39 | "path": "./snippets/dart.json" 40 | }, 41 | { 42 | "language": "xml", 43 | "path": "./snippets/xml.json" 44 | } 45 | ] 46 | }, 47 | "scripts": { 48 | "vscode:prepublish": "webpack --mode production", 49 | "compile-prod": "webpack --mode production", 50 | "compile": "webpack --mode none", 51 | "watch": "webpack --mode none", 52 | "postinstall": "node ./node_modules/vscode/bin/install", 53 | "test-compile": "tsc -p ./" 54 | }, 55 | "devDependencies": { 56 | "@types/mkdirp": "^0.5.2", 57 | "@types/mocha": "^2.2.42", 58 | "@types/node": "^10.12.21", 59 | "@types/q": "^1.5.2", 60 | "@types/rgrove__parse-xml": "^1.1.0", 61 | "mkdirp": "^0.5.1", 62 | "ts-loader": "^6.0.4", 63 | "ts-node": "^8.2.0", 64 | "tslint": "^5.12.1", 65 | "typescript": "^3.3.1", 66 | "vscode": "^1.1.37", 67 | "webpack": "^4.35.3", 68 | "webpack-cli": "^3.3.5" 69 | }, 70 | "dependencies": { 71 | "@rgrove/parse-xml": "^2.0.1", 72 | "q": "^1.5.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /snippets/dart.json: -------------------------------------------------------------------------------- 1 | { 2 | "fxml_app": { 3 | "prefix": "fxml_app", 4 | "body": [ 5 | "import 'package:flutter/material.dart';", 6 | "import 'package:flutter_xmllayout_helpers/providers/PipeProvider.dart';", 7 | "import 'package:provider/provider.dart';", 8 | "import 'package:flutter_localizations/flutter_localizations.dart';", 9 | "import 'pipes/translate.dart';", 10 | "import 'i18n/gen/delegate.dart';", 11 | "", 12 | "", 13 | "void main() async {", 14 | " final RouteObserver routeObserver = new RouteObserver();", 15 | "", 16 | " runApp(", 17 | " MultiProvider(", 18 | " //", 19 | " // dependency injection", 20 | " //", 21 | " providers: [", 22 | " Provider>.value(value: routeObserver),", 23 | " Provider(builder: _createPipeProvider)", 24 | " ],", 25 | " child: ${1:My}App(routeObserver: routeObserver)", 26 | " )", 27 | " );", 28 | "}", 29 | "", 30 | "PipeProvider _createPipeProvider(BuildContext context) {", 31 | " final PipeProvider pipeProvider = PipeProvider();", 32 | "", 33 | " //", 34 | " // register pipes", 35 | " //", 36 | " pipeProvider.register(new TranslatePipe());", 37 | "", 38 | " return pipeProvider;", 39 | "}", 40 | "", 41 | "class ${1:My}App extends StatelessWidget {", 42 | " const ${1:My}App({", 43 | " Key key,", 44 | " this.routeObserver", 45 | " }) : super(key: key);", 46 | "", 47 | " final RouteObserver routeObserver;", 48 | "", 49 | " @override", 50 | " Widget build(BuildContext context) {", 51 | "", 52 | " return MaterialApp(", 53 | " debugShowCheckedModeBanner: false,", 54 | " navigatorObservers: [routeObserver],", 55 | " title: '${2:MyAppTitle}',", 56 | "", 57 | " //", 58 | " // theming", 59 | " //", 60 | " theme: ThemeData(primarySwatch: Colors.blue),", 61 | " ", 62 | " //", 63 | " // localization", 64 | " //", 65 | " localizationsDelegates: [", 66 | " GlobalMaterialLocalizations.delegate,", 67 | " GlobalWidgetsLocalizations.delegate,", 68 | " AppLocalizationsDelegate()", 69 | " ],", 70 | " supportedLocales: [", 71 | " Locale(\"en\")", 72 | " ],", 73 | " locale: Locale(\"en\"),", 74 | " ", 75 | " //", 76 | " // navigation", 77 | " //", 78 | " home: HomePage(),", 79 | " onGenerateRoute: (RouteSettings settings) {", 80 | " switch (settings.name) {", 81 | " case '/':", 82 | " return MaterialPageRoute(builder: (context) => HomePage());", 83 | " }", 84 | " }", 85 | " );", 86 | " }", 87 | "}", 88 | "" 89 | ] 90 | } 91 | } -------------------------------------------------------------------------------- /snippets/xml.json: -------------------------------------------------------------------------------- 1 | { 2 | "fxml_widget": { 3 | "prefix": "fxml_widget", 4 | "body": [ 5 | "<${1:My}Page controller=\"${1:My}Controller\">", 6 | " ", 7 | " ", 8 | " ", 9 | " ", 10 | " <Text text=\"'${2:PageTitle}'\" />", 11 | " ", 12 | " ", 13 | " ", 14 | " ", 15 | " ", 16 | " ", 17 | " ", 18 | " ", 19 | "" 20 | ] 21 | } 22 | } -------------------------------------------------------------------------------- /src/autoclose/autoclose.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | // source: https://github.com/formulahendry/vscode-auto-close-tag 4 | 5 | export function insertAutoCloseTag(event: vscode.TextDocumentChangeEvent): void { 6 | if (!event.contentChanges[0]) { 7 | return; 8 | } 9 | let isRightAngleBracket = CheckRightAngleBracket(event.contentChanges[0]); 10 | if (!isRightAngleBracket && event.contentChanges[0].text !== "/") { 11 | return; 12 | } 13 | 14 | let editor = vscode.window.activeTextEditor; 15 | if (!editor) { 16 | return; 17 | } 18 | 19 | let config = vscode.workspace.getConfiguration('auto-close-tag', editor.document.uri); 20 | if (!config.get("enableAutoCloseTag", true)) { 21 | return; 22 | } 23 | 24 | let languageId = editor.document.languageId; 25 | let languages = config.get("activationOnLanguage", ["*"]); 26 | if (languages.indexOf("*") === -1 && languages.indexOf(languageId) === -1) { 27 | return; 28 | } 29 | 30 | let selection = editor.selection; 31 | let originalPosition = selection.start.translate(0, 1); 32 | let excludedTags = config.get("excludedTags", []); 33 | let isSublimeText3Mode = config.get("SublimeText3Mode", false); 34 | let enableAutoCloseSelfClosingTag = config.get("enableAutoCloseSelfClosingTag", true); 35 | let isFullMode = config.get("fullMode"); 36 | 37 | if ((isSublimeText3Mode || isFullMode) && event.contentChanges[0].text === "/") { 38 | let text = editor.document.getText(new vscode.Range(new vscode.Position(0, 0), originalPosition)); 39 | let last2chars = ""; 40 | if (text.length > 2) { 41 | last2chars = text.substr(text.length - 2); 42 | } 43 | if (last2chars === "") { 48 | closeTag = closeTag.substr(0, closeTag.length - 1); 49 | } 50 | editor.edit((editBuilder) => { 51 | editBuilder.insert(originalPosition, closeTag); 52 | }).then(() => { 53 | if (nextChar === ">") { 54 | (editor as any).selection = moveSelectionRight((editor as any).selection, 1); 55 | } 56 | }); 57 | } 58 | } 59 | } 60 | 61 | if (((!isSublimeText3Mode || isFullMode) && isRightAngleBracket) || 62 | (enableAutoCloseSelfClosingTag && event.contentChanges[0].text === "/")) { 63 | let textLine = editor.document.lineAt(selection.start); 64 | let text = textLine.text.substring(0, selection.start.character + 1); 65 | let result = /<([a-zA-Z][a-zA-Z0-9:\-_.]*)(?:\s+[^<>]*?[^\s/<>=]+?)*?\s?(\/|>)$/.exec(text); 66 | if (result !== null && ((occurrenceCount(result[0], "'") % 2 === 0) 67 | && (occurrenceCount(result[0], "\"") % 2 === 0) && (occurrenceCount(result[0], "`") % 2 === 0))) { 68 | if (result[2] === ">") { 69 | if (excludedTags.indexOf(result[1].toLowerCase()) === -1) { 70 | editor.edit((editBuilder) => { 71 | editBuilder.insert(originalPosition, ""); 72 | }).then(() => { 73 | (editor as any).selection = new vscode.Selection(originalPosition, originalPosition); 74 | }); 75 | } 76 | } else { 77 | if (textLine.text.length <= selection.start.character + 1 || textLine.text[selection.start.character + 1] !== '>') { // if not typing "/" just before ">", add the ">" after "/" 78 | editor.edit((editBuilder) => { 79 | editBuilder.insert(originalPosition, ">"); 80 | }) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | function CheckRightAngleBracket(contentChange: vscode.TextDocumentContentChangeEvent): boolean { 88 | return contentChange.text === ">" || CheckRightAngleBracketInVSCode_1_8(contentChange); 89 | } 90 | 91 | function CheckRightAngleBracketInVSCode_1_8(contentChange: vscode.TextDocumentContentChangeEvent): boolean { 92 | return contentChange.text.endsWith(">") && contentChange.range.start.character === 0 93 | && contentChange.range.start.line === contentChange.range.end.line 94 | && !contentChange.range.end.isEqual(new vscode.Position(0, 0)); 95 | } 96 | 97 | function insertCloseTag(): void { 98 | let editor = vscode.window.activeTextEditor; 99 | if (!editor) { 100 | return; 101 | } 102 | 103 | let selection = editor.selection; 104 | let originalPosition = selection.start; 105 | let config = vscode.workspace.getConfiguration('auto-close-tag', editor.document.uri); 106 | let excludedTags = config.get("excludedTags", []); 107 | let text = editor.document.getText(new vscode.Range(new vscode.Position(0, 0), originalPosition)); 108 | if (text.length > 2) { 109 | let closeTag = getCloseTag(text, excludedTags); 110 | if (closeTag) { 111 | editor.edit((editBuilder) => { 112 | editBuilder.insert(originalPosition, closeTag); 113 | }); 114 | } 115 | } 116 | } 117 | 118 | function getNextChar(editor: vscode.TextEditor, position: vscode.Position): string { 119 | let nextPosition = position.translate(0, 1); 120 | let text = editor.document.getText(new vscode.Range(position, nextPosition)); 121 | return text; 122 | } 123 | 124 | function getCloseTag(text: string, excludedTags: string[]): string { 125 | let regex = /<(\/?[a-zA-Z][a-zA-Z0-9:\-_.]*)(?:\s+[^<>]*?[^\s/<>=]+?)*?\s?>/g; 126 | let result = null; 127 | let stack = []; 128 | while ((result = regex.exec(text)) !== null) { 129 | let isStartTag = result[1].substr(0, 1) !== "/"; 130 | let tag = isStartTag ? result[1] : result[1].substr(1); 131 | if (excludedTags.indexOf(tag.toLowerCase()) === -1) { 132 | if (isStartTag) { 133 | stack.push(tag); 134 | } else if (stack.length > 0) { 135 | let lastTag = stack[stack.length - 1]; 136 | if (lastTag === tag) { 137 | stack.pop() 138 | } 139 | } 140 | } 141 | } 142 | if (stack.length > 0) { 143 | let closeTag = stack[stack.length - 1]; 144 | if (text.substr(text.length - 2) === ""; 146 | } 147 | if (text.substr(text.length - 1) === "<") { 148 | return "/" + closeTag + ">"; 149 | } 150 | return ""; 151 | } else { 152 | return null as any; 153 | } 154 | } 155 | 156 | function moveSelectionRight(selection: vscode.Selection, shift: number): vscode.Selection { 157 | let newPosition = selection.active.translate(0, shift); 158 | let newSelection = new vscode.Selection(newPosition, newPosition); 159 | return newSelection; 160 | } 161 | 162 | function occurrenceCount(source: string, find: string): number { 163 | return source.split(find).length - 1; 164 | } -------------------------------------------------------------------------------- /src/builtin-handlers.ts: -------------------------------------------------------------------------------- 1 | import { PropertyHandlerProvider } from "./providers/property-handler-provider"; 2 | import { FormControlHandler } from "./property-handlers/form-control"; 3 | import { ItemBuilderHandler } from "./property-handlers/item-builder"; 4 | import { ChildBuilderHandler } from "./property-handlers/child-builder"; 5 | import { IfHandler } from "./property-handlers/if"; 6 | import { SwitchHandler } from "./property-handlers/switch"; 7 | import { SwitchCaseHandler } from "./property-handlers/switch-case"; 8 | import { BuilderHandler } from "./property-handlers/builder"; 9 | import { RepeatHandler } from "./property-handlers/repeat"; 10 | import { DecorationValueTransformer } from "./value-transformers/decoration"; 11 | import { WrapperStreamPropertyHandler } from "./property-handlers/wrapper-stream-property"; 12 | import { WrapperDisablePropertyHandler } from "./property-handlers/wrapper-disable-property"; 13 | import { WrapperAnimationHandler } from "./property-handlers/wrapper-animation"; 14 | import { WrapperPropertyHandler } from "./property-handlers/wrapper-property"; 15 | import { ChildWrapperPropertyHandler } from "./property-handlers/child-wrapper-property"; 16 | import { ValueTransformersProvider } from "./providers/value-transformers-provider"; 17 | import { EdgeInsetsValueTransformer } from "./value-transformers/edge-insets"; 18 | import { ColorValueTransformer } from "./value-transformers/color"; 19 | import { EnumValueTransformer } from "./value-transformers/enum"; 20 | import { IfElementHandler } from "./property-handlers/if-element"; 21 | import { WrapperConsumerPropertyHandler } from "./property-handlers/wrapper-consumer-property"; 22 | import { ItemBuilderPropertyHandler } from "./property-handlers/item-builder-property"; 23 | import { FormGroupHandler } from "./property-handlers/form-group"; 24 | import { FormSubmitHandler } from "./property-handlers/form-submit"; 25 | import { PropertyResolver } from "./resolvers/property-resolver"; 26 | 27 | 28 | export function registerBuiltInPropertyHandlers(provider: PropertyHandlerProvider, propertyResolver: PropertyResolver) { 29 | // 30 | // custom property handlers 31 | // 32 | provider.register('builder', new BuilderHandler(propertyResolver)); 33 | provider.register('itemBuilder', new ItemBuilderHandler(propertyResolver)); 34 | provider.register(':itemBuilder', new ItemBuilderPropertyHandler(propertyResolver)); 35 | provider.register(':childBuilder', new ChildBuilderHandler(propertyResolver)); // repeat the content and put them in children property .... 37 | provider.register(':if', new IfHandler(propertyResolver)); 38 | provider.register('if', new IfElementHandler(propertyResolver)); 39 | provider.register(':switch', new SwitchHandler(propertyResolver)); 40 | provider.register(':switchCase', new SwitchCaseHandler()); 41 | provider.register(':formControl', new FormControlHandler(propertyResolver)); 42 | provider.register(':formGroup', new FormGroupHandler()); 43 | provider.register(':formSubmit', new FormSubmitHandler(propertyResolver)); 44 | 45 | // 46 | // child wrappers (the target widget must have a child property) 47 | // 48 | provider.register(':padding', new ChildWrapperPropertyHandler(propertyResolver, [{ handler: ':padding', targetProperty: 'padding' }], 'Padding')); 49 | provider.register(':text', new ChildWrapperPropertyHandler(propertyResolver, [{ handler: ':text', targetProperty: '' }], 'Text', undefined, -1000000)); // must be the lowest priority 50 | provider.register(':icon', new ChildWrapperPropertyHandler(propertyResolver, [{ handler: ':icon', targetProperty: 'icon' }], 'Icon', undefined, -1000000)); // must be the lowest priority 51 | 52 | // 53 | // wrapper properties 54 | // 55 | provider.register(':margin', new WrapperPropertyHandler(propertyResolver, [{ handler: ':margin', targetProperty: 'padding' }], 'Padding')); 56 | provider.register(':opacity', new WrapperPropertyHandler(propertyResolver, [{ handler: ':opacity', targetProperty: 'opacity' }], 'Opacity')); 57 | provider.register(':visible', new WrapperPropertyHandler(propertyResolver, [{ handler: ':visible', targetProperty: 'visible' }], 'Visibility')); 58 | provider.register(':hero', new WrapperPropertyHandler(propertyResolver, [{ handler: ':hero', targetProperty: 'tag' }], 'Hero')); 59 | provider.register(':aspectRatio', new WrapperPropertyHandler(propertyResolver, [{ handler: ':aspectRatio', targetProperty: 'aspectRatio' }], 'AspectRatio')); 60 | provider.register(':center', new WrapperPropertyHandler(propertyResolver, [{ handler: ':center', targetProperty: '' }], 'Center')); 61 | provider.register(':align', new WrapperPropertyHandler(propertyResolver, [{ handler: ':align', targetProperty: 'alignment' }], 'Align')); 62 | provider.register(':flex', new WrapperPropertyHandler(propertyResolver, [{ handler: ':flex', targetProperty: 'flex' }], 'Expanded')); 63 | provider.register([':width', ':height'], new WrapperPropertyHandler(propertyResolver, [{ handler: ':width', targetProperty: 'width' }, { handler: ':height', targetProperty: 'height' }], 'SizedBox')); 64 | provider.register(':theme', new WrapperPropertyHandler(propertyResolver, [{ handler: ':theme', targetProperty: 'data' }], 'Theme')); 65 | provider.register(':translate', new WrapperPropertyHandler(propertyResolver, [{ handler: ':translate', targetProperty: 'offset' }], 'Transform.translate')); 66 | provider.register(':scale', new WrapperPropertyHandler(propertyResolver, [{ handler: ':scale', targetProperty: 'scale' }], 'Transform.scale')); 67 | provider.register(':rotate', new WrapperPropertyHandler(propertyResolver, [{ handler: ':rotate', targetProperty: 'angle' }], 'Transform.rotate')); 68 | // provider.register(':textDir', new WrapperPropertyHandler(propertyResolver, [{ handler: ':textDir', targetProperty: 'textDirection' }], 'Directionality')); 69 | 70 | 71 | // 72 | // custom wrappers 73 | // 74 | provider.register(':consumer', new WrapperConsumerPropertyHandler(propertyResolver)); 75 | provider.register(':stream', new WrapperStreamPropertyHandler(propertyResolver)); 76 | provider.register(':disable', new WrapperDisablePropertyHandler(propertyResolver)); 77 | provider.register('apply-animation', new WrapperAnimationHandler(propertyResolver)); 78 | 79 | // 80 | // wrapper events 81 | // 82 | provider.register([':onTap', ':onDoubleTap', ':onLongPress'], new WrapperPropertyHandler(propertyResolver, [ 83 | { handler: ':onTap', targetProperty: 'onTap' }, 84 | { handler: ':onDoubleTap', targetProperty: 'onDoubleTap' }, 85 | { handler: ':onLongPress', targetProperty: 'onLongPress' } 86 | ], 'GestureDetector')); 87 | } 88 | 89 | export function registerBuiltInValueTransformers(provider: ValueTransformersProvider) { 90 | // 91 | // custom values 92 | // 93 | provider.register(['padding', 'margin'], new EdgeInsetsValueTransformer()); 94 | provider.register(['color', 'background', 'backgroundColor', 'decorationColor'], new ColorValueTransformer()); 95 | provider.register(['decoration'], new DecorationValueTransformer()); 96 | 97 | // 98 | // enum and constant values 99 | // 100 | provider.register(['fontWeight'], new EnumValueTransformer('FontWeight')); 101 | provider.register(['textAlign'], new EnumValueTransformer('TextAlign')); 102 | provider.register(['textBaseline'], new EnumValueTransformer('TextBaseline')); 103 | provider.register(['textDirection'], new EnumValueTransformer('TextDirection')); 104 | provider.register(['verticalDirection'], new EnumValueTransformer('VerticalDirection')); 105 | provider.register(['mainAxisAlignment'], new EnumValueTransformer('MainAxisAlignment')); 106 | provider.register(['crossAxisAlignment'], new EnumValueTransformer('CrossAxisAlignment')); 107 | provider.register(['mainAxisSize'], new EnumValueTransformer('MainAxisSize')); 108 | provider.register(['axis', 'scrollDirection'], new EnumValueTransformer('Axis')); 109 | provider.register(['icon'], new EnumValueTransformer('Icons')); 110 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import Manager from './manager'; 3 | import * as fs from 'fs'; 4 | import { denodeify } from 'q'; 5 | import { Config } from './models/config'; 6 | import { languages } from 'vscode'; 7 | import { DartReferenceProvider } from './language-features/providers/dart_reference_provider'; 8 | import { DartHoverProvider } from './language-features/providers/dart_hover_provider'; 9 | import { DartCompletionItemProvider } from './language-features/providers/dart_completion_item_provider'; 10 | import { FixCodeActionProvider } from './language-features/providers/fix_code_action_provider'; 11 | import { RankingCodeActionProvider } from './language-features/providers/ranking_code_action_provider'; 12 | import { DartDiagnosticProvider } from './language-features/providers/dart_diagnostic_provider'; 13 | 14 | const readFile = denodeify(fs.readFile); 15 | 16 | export async function activate(context: vscode.ExtensionContext) { 17 | let config: Config = {}; 18 | const configFiles = await vscode.workspace.findFiles('fxmllayout.json'); 19 | if (configFiles.length) { 20 | const json = await readFile(configFiles[0].fsPath, 'utf8') as string; 21 | config = JSON.parse(json); 22 | } 23 | 24 | const diagnostics = languages.createDiagnosticCollection('XML_LAYOUT_FOR_FLUTTER'); 25 | const manager = new Manager(config, diagnostics); 26 | 27 | context.subscriptions.push( 28 | vscode.commands.registerCommand('flutter.xml-layout.regenerate-all', async () => { 29 | await manager.regenerateAll(); 30 | }) 31 | ); 32 | 33 | // 34 | // language support features 35 | // 36 | 37 | const activeFileFilters = [{ language: "xml", scheme: "file" }]; 38 | const triggerCharacters = "<\"' /:.".split(""); 39 | 40 | // code action providers 41 | const rankingCodeActionProvider = new RankingCodeActionProvider(); 42 | rankingCodeActionProvider.registerProvider(new FixCodeActionProvider(activeFileFilters)); 43 | 44 | const completionItemProvider = new DartCompletionItemProvider(manager.propertyHandlersProvider, manager.propertyResolver); 45 | const hoverProvider = new DartHoverProvider(); 46 | const referenceProvider = new DartReferenceProvider(); 47 | 48 | // other providers 49 | context.subscriptions.push(languages.registerCompletionItemProvider(activeFileFilters, completionItemProvider, ...triggerCharacters)); 50 | context.subscriptions.push(languages.registerHoverProvider(activeFileFilters, hoverProvider)); 51 | context.subscriptions.push(languages.registerDefinitionProvider(activeFileFilters, referenceProvider)); 52 | context.subscriptions.push(languages.registerReferenceProvider(activeFileFilters, referenceProvider)); 53 | context.subscriptions.push(languages.registerCodeActionsProvider(activeFileFilters, rankingCodeActionProvider, rankingCodeActionProvider.metadata)); 54 | 55 | // diagnostics 56 | const diagnosticsProvider = new DartDiagnosticProvider(diagnostics); 57 | context.subscriptions.push(languages.onDidChangeDiagnostics((e) => diagnosticsProvider.onDidChangeDiagnostics(e))); 58 | context.subscriptions.push(diagnostics); 59 | } 60 | 61 | export function deactivate(isRestart: boolean = false) { 62 | } 63 | -------------------------------------------------------------------------------- /src/generators/localization-generator.ts: -------------------------------------------------------------------------------- 1 | 2 | export class LocalizationGenerator { 3 | 4 | generateDelegate(supportedLangs: string[]): string { 5 | let code = 6 | `import 'package:flutter/foundation.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | import 'localizations.dart'; 9 | 10 | class AppLocalizationsDelegate extends LocalizationsDelegate { 11 | const AppLocalizationsDelegate(); 12 | 13 | @override 14 | bool isSupported(Locale locale) => [${supportedLangs.map(a => `"${a}"`).join(', ')}].contains(locale.languageCode); 15 | 16 | @override 17 | Future load(Locale locale) { 18 | // Returning a SynchronousFuture here because an async "load" operation 19 | // isn't needed to produce an instance of AppLocalizations. 20 | return SynchronousFuture(AppLocalizations(locale)); 21 | } 22 | 23 | @override 24 | bool shouldReload(AppLocalizationsDelegate old) => false; 25 | }`; 26 | return code; 27 | } 28 | 29 | generateLocalization(langs: { [name: string]: string }): string { 30 | const langsTranslation = this.generateLangsTranslation(langs); 31 | let code = 32 | `import 'package:flutter/widgets.dart'; 33 | 34 | class AppLocalizations { 35 | AppLocalizations(this.locale); 36 | 37 | final Locale locale; 38 | 39 | String getTranslation(String key) { 40 | final lang = _localizedValues[locale.languageCode]; 41 | if (lang != null) { 42 | return lang[key]; 43 | } 44 | return null; 45 | } 46 | 47 | static AppLocalizations of(BuildContext context) { 48 | return Localizations.of(context, AppLocalizations); 49 | } 50 | 51 | static Map> _localizedValues = { 52 | ${langsTranslation.join(',\n ')} 53 | }; 54 | }`; 55 | return code; 56 | } 57 | 58 | private generateLangsTranslation(langs: { [name: string]: string; }): string[] { 59 | const codes: string[] = []; 60 | Object.keys(langs).forEach(lang => { 61 | const data = JSON.parse(langs[lang]); 62 | const code = `"${lang}": {\n ${this.generateLangTranslation(data)}\n }`; 63 | codes.push(code); 64 | }); 65 | return codes; 66 | } 67 | 68 | private generateLangTranslation(data: any): string { 69 | const codes: string[] = []; 70 | 71 | Object.keys(data).forEach(key => { 72 | codes.push(`"${key}": "${data[key]}"`); 73 | }); 74 | 75 | return codes.join(',\n '); 76 | } 77 | } -------------------------------------------------------------------------------- /src/generators/widget-generator.ts: -------------------------------------------------------------------------------- 1 | import { RootWidgetModel, WidgetModel, PropertyModel, VariableModel, FormControlModel } from "../models/models"; 2 | import { makeTabs, sortProperties } from "../utils"; 3 | import { PropertyHandlerProvider } from "../providers/property-handler-provider"; 4 | 5 | 6 | export class WidgetCodeGenerator { 7 | private readonly propertyHandlerProvider: PropertyHandlerProvider; 8 | 9 | constructor(propertyHandlerProvider: PropertyHandlerProvider) { 10 | this.propertyHandlerProvider = propertyHandlerProvider; 11 | } 12 | 13 | generateWidgetCode(widget: WidgetModel, tabsLevel: number): string { 14 | if (!widget) { 15 | return ''; 16 | } 17 | 18 | // custom generated code for custom widgets e.g. (if) 19 | const customGenerator = this.propertyHandlerProvider.get(widget.type); 20 | if (customGenerator && customGenerator.canGenerate(widget)) { 21 | return customGenerator.generate(widget, tabsLevel, (w, l) => this.generateWidgetCode(w, l), (w, p, l) => this.generatePropertyCode(w, p, l)); 22 | } 23 | 24 | let props: string[] = []; 25 | const tabs = makeTabs(tabsLevel); 26 | 27 | const constProp = widget.properties.find(a => a.name === 'const'); 28 | const properties = widget.properties.sort(sortProperties).filter(a => !a.skipGeneratingCode && a.name !== 'const'); 29 | 30 | for (const prop of properties) { 31 | let propCode = this.generatePropertyCode(widget, prop, tabsLevel + (widget.isPropertyElement ? 0: 1)); 32 | if (propCode) { 33 | props.push(propCode); 34 | } 35 | } 36 | 37 | let code = ''; 38 | 39 | if (widget.comments && widget.comments.length) { 40 | code = widget.comments.map(a => a.trim()).filter(a => !!a).join('\n' + tabs) + '\n'; 41 | } 42 | 43 | const constCode = constProp && (constProp.value === '' || constProp.value === 'true') ? 'const ' : ''; 44 | const propsCode = props.filter(a => a.trim()).join(',\n'); 45 | 46 | if (!widget.isPropertyElement && widget.type) { 47 | code += `${constCode}${widget.type}(\n${propsCode}${propsCode.trim() ? ',' : ''}\n${tabs})`; 48 | } 49 | else { 50 | code += propsCode; 51 | } 52 | return code; 53 | } 54 | 55 | private generatePropertyCode(widget: WidgetModel, prop: PropertyModel, tabsLevel: number, addTabsAtStart = true): string { 56 | const customGenerator = this.propertyHandlerProvider.get(prop.name); 57 | if (customGenerator && customGenerator.canGenerate(widget)) { 58 | return customGenerator.generate(widget, tabsLevel, (w, l) => this.generateWidgetCode(w, l), (w, p, l) => this.generatePropertyCode(w, p, l)); 59 | } 60 | 61 | if (prop.generateCodeDelegate) { 62 | return prop.generateCodeDelegate(widget, prop, tabsLevel); 63 | } 64 | 65 | if (!prop.value && (prop.dataType === 'widget' || prop.dataType === 'widgetList')) { 66 | return ''; 67 | } 68 | 69 | const tabs = makeTabs(tabsLevel); 70 | let code = addTabsAtStart && !widget.isPropertyElement ? tabs : ''; 71 | 72 | if (prop.name && !widget.isPropertyElement) { 73 | code += `${prop.name}: `; 74 | } 75 | 76 | switch (prop.dataType) { 77 | case 'string': 78 | case 'object': 79 | code += `${prop.value}`; 80 | break; 81 | 82 | case 'propertyElement': 83 | code += `${this.generatePropertyCode(widget, prop.value as any, tabsLevel, false)}`; 84 | break; 85 | 86 | case 'widget': 87 | code += this.generateWidgetCode(prop.value as WidgetModel, tabsLevel); 88 | break; 89 | 90 | case 'widgetList': 91 | code += `[\n${tabs} `; 92 | (prop.value as WidgetModel[]).forEach((widget, i) => { 93 | code += this.generateWidgetCode(widget, tabsLevel + 1) + `,\n${tabs}`; 94 | if (i + 1 < (prop.value as WidgetModel[]).length) { 95 | code += ' '; 96 | } 97 | }); 98 | code += `]`; 99 | break; 100 | 101 | case 'function': 102 | { 103 | const contentTabs = makeTabs(tabsLevel + 1); 104 | const data = prop.extraData || {}; 105 | const params = data.parameters || []; 106 | code += `(${params.map(a => `${a.type ? a.type + ' ' : ''}${a.name}`).join(', ')}) {`; 107 | 108 | // const vars = data.variables || []; 109 | // code += `${vars.map(a => `${contentTabs}final ${a.type ? a.type + ' ' : ''}${a.name} = ${a.value};`).join('\n')}`; 110 | 111 | const logic: string[] = data.logic || []; 112 | code += `${logic.length ? '\n' : ''}${logic.map(a => `${contentTabs}${a}`).join('\n')}`; 113 | 114 | const childWidget = widget.wrappedWidgets[0]; 115 | if (childWidget) { 116 | const content = this.generateWidgetCode(childWidget, tabsLevel + 1); 117 | code += `\n${contentTabs}${data.addReturn ? 'return ' : ''}${content}${data.addReturn ? ';' : ''}\n`; 118 | } 119 | 120 | code += `${tabs}}`; 121 | } 122 | break; 123 | } 124 | 125 | return code; 126 | } 127 | } -------------------------------------------------------------------------------- /src/language-features/dart-ext-types.ts: -------------------------------------------------------------------------------- 1 | import { Position } from "vscode"; 2 | 3 | 4 | export type FilePath = string; 5 | 6 | /** 7 | * A location (character range) within a file. 8 | */ 9 | export interface Location { 10 | file: FilePath; 11 | offset: number; 12 | length: number; 13 | startLine: number; 14 | startColumn: number; 15 | } 16 | /** 17 | * A description of a set of edits that implement a single conceptual change. 18 | */ 19 | export interface SourceChange { 20 | /** 21 | * A human-readable description of the change to be applied. 22 | */ 23 | message: string; 24 | 25 | /** 26 | * A list of the edits used to effect the change, grouped by file. 27 | */ 28 | edits: SourceFileEdit[]; 29 | 30 | /** 31 | * A list of the linked editing groups used to customize the changes that 32 | * were made. 33 | */ 34 | linkedEditGroups: LinkedEditGroup[]; 35 | 36 | /** 37 | * The position that should be selected after the edits have been 38 | * applied. 39 | */ 40 | selection?: Position; 41 | 42 | /** 43 | * The optional identifier of the change kind. The identifier remains 44 | * stable even if the message changes, or is parameterized. 45 | */ 46 | id?: string; 47 | } 48 | export interface SourceFileEdit { 49 | /** 50 | * The file containing the code to be modified. 51 | */ 52 | file: FilePath; 53 | 54 | /** 55 | * The modification stamp of the file at the moment when the change was 56 | * created, in milliseconds since the "Unix epoch". Will be -1 if the 57 | * file did not exist and should be created. The client may use this 58 | * field to make sure that the file was not changed since then, so it is 59 | * safe to apply the change. 60 | */ 61 | fileStamp: number; 62 | 63 | /** 64 | * A list of the edits used to effect the change. 65 | */ 66 | edits: SourceEdit[]; 67 | } 68 | export interface SourceEdit { 69 | /** 70 | * The offset of the region to be modified. 71 | */ 72 | offset: number; 73 | 74 | /** 75 | * The length of the region to be modified. 76 | */ 77 | length: number; 78 | 79 | /** 80 | * The code that is to replace the specified region in the original code. 81 | */ 82 | replacement: string; 83 | 84 | /** 85 | * An identifier that uniquely identifies this source edit from other 86 | * edits in the same response. This field is omitted unless a containing 87 | * structure needs to be able to identify the edit for some reason. 88 | * 89 | * For example, some refactoring operations can produce edits that might 90 | * not be appropriate (referred to as potential edits). Such edits will 91 | * have an id so that they can be referenced. Edits in the same response 92 | * that do not need to be referenced will not have an id. 93 | */ 94 | id?: string; 95 | } 96 | export interface LinkedEditGroup { 97 | /** 98 | * The positions of the regions that should be edited simultaneously. 99 | */ 100 | positions: Position[]; 101 | 102 | /** 103 | * The length of the regions that should be edited simultaneously. 104 | */ 105 | length: number; 106 | 107 | /** 108 | * Pre-computed suggestions for what every region might want to be 109 | * changed to. 110 | */ 111 | suggestions: LinkedEditSuggestion[]; 112 | } 113 | 114 | /** 115 | * A suggestion of a value that could be used to replace all of the linked 116 | * edit regions in a LinkedEditGroup. 117 | */ 118 | export interface LinkedEditSuggestion { 119 | /** 120 | * The value that could be used to replace all of the linked edit 121 | * regions. 122 | */ 123 | value: string; 124 | 125 | /** 126 | * The kind of value being proposed. 127 | */ 128 | kind: LinkedEditSuggestionKind; 129 | } 130 | 131 | /** 132 | * An enumeration of the kind of values that can be suggested for a linked 133 | * edit. 134 | */ 135 | export type LinkedEditSuggestionKind = 136 | "METHOD" 137 | | "PARAMETER" 138 | | "TYPE" 139 | | "VARIABLE"; 140 | 141 | -------------------------------------------------------------------------------- /src/language-features/providers/dart_diagnostic_provider.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, DiagnosticCollection, DiagnosticSeverity, Uri, TextDocument, workspace, languages, DiagnosticChangeEvent } from "vscode"; 2 | import { getXmlCodeWordLocation } from "../utils"; 3 | 4 | export class DartDiagnosticProvider { 5 | constructor(private readonly diagnostics: DiagnosticCollection) { 6 | } 7 | 8 | async onDidChangeDiagnostics(event: DiagnosticChangeEvent) { 9 | const file = event.uris.filter(a => a.path.endsWith('.xml.dart'))[0]; 10 | if (file) { 11 | const dartDocument = await workspace.openTextDocument(Uri.file(file.path)); 12 | const dartCode = dartDocument.getText(); 13 | const xmlFile = file.path.replace('.xml.dart', '.xml'); 14 | const xmlDocument = await workspace.openTextDocument(Uri.file(xmlFile)); 15 | const results = languages.getDiagnostics(file).filter(a => a.severity === DiagnosticSeverity.Error); 16 | const allMessages: string[] = []; 17 | const mappedErrors = results 18 | .map((e) => DartDiagnosticProvider.createDiagnostic(dartCode, dartDocument, xmlDocument, e)) 19 | .filter(a => !!a) 20 | .filter(a => { 21 | // remove duplicated diagnostics 22 | const res = !allMessages.find(m => m === a.message); 23 | if (res) { 24 | allMessages.push(a.message); 25 | } 26 | return res; 27 | }); 28 | this.diagnostics.set( 29 | Uri.file(xmlFile), 30 | mappedErrors 31 | ); 32 | } 33 | } 34 | 35 | public static createDiagnostic(dartCode: string, dartDocument: TextDocument, xmlDocument: TextDocument, error: Diagnostic): Diagnostic { 36 | const offset = dartDocument.offsetAt(error.range.start); 37 | const offsetEnd = dartDocument.offsetAt(error.range.end); 38 | const xmlWordRange = getXmlCodeWordLocation(xmlDocument, dartCode, 39 | { offset: offset, length: offsetEnd - offset } as any); 40 | if (!xmlWordRange) { 41 | return null; 42 | } 43 | error.range = xmlWordRange; 44 | (error as any).location = { offset }; 45 | return error; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/language-features/providers/dart_hover_provider.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, Hover, HoverProvider, Position, Range, TextDocument, Uri, commands } from "vscode"; 2 | import { getDartDocument, getDartCodeIndex } from "../utils"; 3 | 4 | export class DartHoverProvider implements HoverProvider { 5 | constructor() { } 6 | 7 | public async provideHover(xmlDocument: TextDocument, xmlPosition: Position, token: CancellationToken): Promise { 8 | try { 9 | const wordRange = xmlDocument.getWordRangeAtPosition(xmlPosition); 10 | if (!wordRange) { 11 | return; 12 | } 13 | 14 | const dartDocument = await getDartDocument(xmlDocument); 15 | const dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange, true); 16 | if (dartOffset < 0) { 17 | return; 18 | } 19 | 20 | const results: Hover[] = await commands.executeCommand('vscode.executeHoverProvider', dartDocument.uri, dartDocument.positionAt(dartOffset)); 21 | const contents: any[] = results && results[0] && results[0].contents; 22 | if (!contents) { 23 | return undefined; 24 | } 25 | // fixes bug that prevents the hover from showing 26 | const hover = new Hover([ 27 | // { language: 'dart', value: (data.contents[0] as any).value }, 28 | (typeof contents[0] !== 'string' ? contents[0] && (contents[0] as any).value : contents[0]) || undefined, 29 | (typeof contents[1] !== 'string' ? contents[1] && (contents[1] as any).value : contents[1]) || undefined 30 | ]); 31 | return hover; 32 | } catch (e) { 33 | console.error(e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/language-features/providers/dart_reference_provider.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, DefinitionLink, DefinitionProvider, Location, Position, ReferenceContext, ReferenceProvider, TextDocument, Uri, Range, workspace, commands } from "vscode"; 2 | import * as util from "../utils"; 3 | import { getDartDocument, getDartCodeIndex } from "../utils"; 4 | 5 | export class DartReferenceProvider implements ReferenceProvider, DefinitionProvider { 6 | constructor() { } 7 | 8 | public async provideReferences(xmlDocument: TextDocument, xmlPosition: Position, context: ReferenceContext, token: CancellationToken): Promise { 9 | const wordRange = xmlDocument.getWordRangeAtPosition(xmlPosition); 10 | if (!wordRange) { 11 | return; 12 | } 13 | 14 | const dartDocument = await getDartDocument(xmlDocument); 15 | const dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange); 16 | let dartPosition = null; 17 | 18 | if (dartOffset === -1) { 19 | return; 20 | } 21 | 22 | // If we want to include the decleration, kick off a request for that. 23 | const definitions = context.includeDeclaration && dartPosition 24 | ? await this.provideDefinition(dartDocument, dartPosition, token) 25 | : undefined; 26 | 27 | const locations: Location[] = await commands.executeCommand('vscode.executeReferenceProvider', dartDocument.uri, dartDocument.positionAt(dartOffset)); 28 | 29 | if (token && token.isCancellationRequested) 30 | return; 31 | 32 | return definitions 33 | ? locations.concat(definitions.map((dl) => new Location(dl.targetUri, dl.targetRange))) 34 | : locations; 35 | } 36 | 37 | public async provideDefinition(xmlDocument: TextDocument, xmlPosition: Position, token: CancellationToken): Promise { 38 | const wordRange = xmlDocument.getWordRangeAtPosition(xmlPosition); 39 | if (!wordRange) { 40 | return; 41 | } 42 | 43 | const dartDocument = await getDartDocument(xmlDocument); 44 | const dartOffset = getDartCodeIndex(xmlDocument, xmlPosition, dartDocument, wordRange); 45 | 46 | const results: Location[] = await commands.executeCommand('vscode.executeDefinitionProvider', dartDocument.uri, dartDocument.positionAt(dartOffset)); 47 | return results.map(loc => { 48 | const range: Range = loc.range || (loc as any).targetRange; // targetRange in the new versions of vscode 49 | const offsetStart = dartDocument.offsetAt(range.start); 50 | const offsetEnd = dartDocument.offsetAt(range.end); 51 | const target = { startColumn: range.start.character + 1, startLine: range.start.line + 1, length: offsetEnd - offsetStart }; 52 | 53 | if (target.startColumn === 0) 54 | target.startColumn = 1; 55 | 56 | const uri = loc.uri || (loc as any).targetUri; // targetUri in the new versions of vscode 57 | let file = uri.fsPath; 58 | if (file.endsWith('.xml.dart')) { 59 | file = file.replace('.xml.dart', '.xml'); 60 | target.startColumn = 1; 61 | target.startLine = 1; 62 | } 63 | 64 | return { 65 | originSelectionRange: util.toRange(dartDocument, offsetStart, offsetEnd - offsetStart), 66 | targetRange: util.toRangeOnLine(target as any), 67 | targetUri: Uri.file(file) 68 | } as DefinitionLink; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/language-features/providers/fix_code_action_provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | CodeAction, 4 | CodeActionContext, 5 | CodeActionKind, 6 | CodeActionProviderMetadata, 7 | Command, 8 | DocumentSelector, 9 | Range, 10 | TextDocument, 11 | TextEdit, 12 | WorkspaceEdit, 13 | commands, 14 | } from 'vscode'; 15 | import { Location, SourceChange } from '../dart-ext-types'; 16 | 17 | import { RankedCodeActionProvider } from './ranking_code_action_provider'; 18 | import { getDartDocument } from '../utils'; 19 | 20 | export class FixCodeActionProvider implements RankedCodeActionProvider { 21 | constructor(public readonly selector: DocumentSelector) { } 22 | 23 | public readonly rank = 1; 24 | 25 | public readonly metadata: CodeActionProviderMetadata = { 26 | providedCodeActionKinds: [CodeActionKind.QuickFix], 27 | }; 28 | 29 | public async provideCodeActions(xmlDocument: TextDocument, xmlRange: Range, context: CodeActionContext, token: CancellationToken): Promise { 30 | const dartDocument = await getDartDocument(xmlDocument); 31 | const location = ((context.diagnostics[0] || {}) as any).location as Location; 32 | if (!location || location.offset === -1) { 33 | return undefined; 34 | } 35 | 36 | if (context && context.only && !context.only.contains(CodeActionKind.QuickFix)) { 37 | return undefined; 38 | } 39 | 40 | try { 41 | const rangeStart = dartDocument.positionAt(location.offset); 42 | let results: (Command | CodeAction)[] = await commands.executeCommand('vscode.executeCodeActionProvider', dartDocument.uri, new Range(rangeStart, rangeStart.translate({ characterDelta: 10 }))); 43 | return results.map(a => this.buildCodeAction(xmlDocument, a)); 44 | } 45 | catch (e) { 46 | console.error(e); 47 | throw e; 48 | } 49 | } 50 | 51 | private buildCodeAction(document: TextDocument, command: Command | CodeAction): CodeAction { 52 | const innerCommand = command.command as any; 53 | const title = innerCommand && innerCommand.title ? innerCommand.title : command.title; 54 | if (!title || !title.startsWith('Import library')) { 55 | return null; 56 | } 57 | 58 | // once we have a title like "Import library 'package:xxx.xml.dart'", we can build a CodeAction to import file 59 | const change: SourceChange = { 60 | message: 'import library', 61 | edits: [ 62 | { 63 | file: document.uri.path, 64 | fileStamp: -1, 65 | edits: [ 66 | { 67 | offset: 0, 68 | length: 0, 69 | replacement: title, 70 | }, 71 | ], 72 | }, 73 | ], 74 | linkedEditGroups: [], 75 | }; 76 | this.buildImportNamespace(document, change); 77 | 78 | const action = new CodeAction(title, CodeActionKind.QuickFix); 79 | action.edit = new WorkspaceEdit(); 80 | const edit = new TextEdit( 81 | new Range(document.positionAt(change.edits[0].edits[0].offset), document.positionAt(change.edits[0].edits[0].offset + change.edits[0].edits[0].length)), 82 | change.edits[0].edits[0].replacement 83 | ); 84 | action.edit.set(document.uri, [edit]); 85 | return action; 86 | } 87 | 88 | private buildImportNamespace(document: TextDocument, change: SourceChange) { 89 | change.edits[0].edits[0].offset = document.getText().indexOf('>'); 90 | change.edits[0].file = change.edits[0].file.replace('.xml.dart', '.xml'); 91 | const importText = this.extractImportText(change.edits[0].edits[0].replacement); 92 | const namespace = importText.substring(importText.lastIndexOf('/') + 1).replace('.dart', '').toLowerCase(); 93 | change.edits[0].edits[0].replacement = '\n\txmlns:' + namespace + '="' + importText + '"'; 94 | } 95 | 96 | private extractImportText(text: string): string { 97 | const quoteIndex = text.indexOf("'"); 98 | const lastQuoteIndex = text.lastIndexOf("'"); 99 | text = text.substring(quoteIndex + 1, lastQuoteIndex); 100 | return text; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/language-features/providers/ranking_code_action_provider.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, CodeAction, CodeActionContext, CodeActionProvider, CodeActionProviderMetadata, Command, DocumentSelector, languages, Range, TextDocument } from "vscode"; 2 | import { flatMap, uniq, sortBy } from "../utils"; 3 | 4 | export class RankingCodeActionProvider implements CodeActionProvider { 5 | private codeActionProviders: RankedCodeActionProvider[] = []; 6 | 7 | public registerProvider(provider: RankedCodeActionProvider): void { 8 | this.codeActionProviders.push(provider); 9 | sortBy(this.codeActionProviders, (p) => p.rank); 10 | } 11 | 12 | get metadata(): CodeActionProviderMetadata { 13 | const allKinds = flatMap(this.codeActionProviders, (p) => p.metadata.providedCodeActionKinds); 14 | return { providedCodeActionKinds: uniq(allKinds) }; 15 | } 16 | 17 | public async provideCodeActions(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): Promise> { 18 | // Sort the providers, because then their results will be sorted (flatMap doesn't change the order, and 19 | // Promise.all preserves order). 20 | const applicableProviders = this.codeActionProviders.filter((p) => languages.match(p.selector, document)); 21 | const promises = applicableProviders.map((p) => p.provideCodeActions(document, range, context, token)); 22 | const allResults = await Promise.all(promises); 23 | const flatResults = flatMap(allResults, (x) => x); 24 | return flatResults; 25 | } 26 | } 27 | 28 | export type RankedCodeActionProvider = 29 | CodeActionProvider 30 | & { selector: DocumentSelector } 31 | & { metadata: CodeActionProviderMetadata } 32 | & { rank: number }; 33 | -------------------------------------------------------------------------------- /src/language-features/xmlUtils.ts: -------------------------------------------------------------------------------- 1 | import {Position, Range, TextDocument} from 'vscode'; 2 | 3 | // source: https://github.com/Tyriar/vscode-xml 4 | 5 | // This will catch: 6 | // * Start tags: 9 | const startTagPattern = '<\s*[\\.\\-:_a-zA-Z0-9]+'; 10 | const endTagPattern = '<\\/\s*[\\.\\-:_a-zA-Z0-9]+'; 11 | const autoClosePattern = '\\/>'; 12 | const startCommentPattern = '\s*'; 14 | const fullPattern = new RegExp("(" + 15 | startTagPattern + "|" + endTagPattern + "|" + autoClosePattern + "|" + 16 | startCommentPattern + "|" + endCommentPattern + ")", "g"); 17 | 18 | 19 | // Get the full XPath to the current tag. 20 | export function getXPath(document: TextDocument, position: Position): string[] { 21 | // For every row, checks if it's an open, close, or autoopenclose tag and 22 | // update a list of all the open tags. 23 | //{row, column} = bufferPosition 24 | const xpath: string[] = []; 25 | const skipList: string[] = []; 26 | let waitingStartTag = false; 27 | let waitingStartComment = false; 28 | 29 | // For the first line read, excluding the word the cursor is over 30 | const wordRange = document.getWordRangeAtPosition(position); 31 | const wordStart = wordRange ? wordRange.start : position; 32 | let line = document.getText(new Range(position.line, 0, position.line, wordStart.character)); 33 | let row = position.line; 34 | 35 | while (row >= 0) { //and (!maxDepth or xpath.length < maxDepth) 36 | row--; 37 | 38 | // Apply the regex expression, read from right to left. 39 | let matches = line.match(fullPattern); 40 | if (matches) { 41 | matches.reverse(); 42 | 43 | for (let i = 0; i < matches.length; i++) { 44 | let match = matches[i]; 45 | let tagName; 46 | 47 | // Start comment 48 | if (match === "") { 53 | waitingStartComment = true; 54 | } 55 | // Omit comment content 56 | else if (waitingStartComment) { 57 | continue; 58 | } 59 | // Auto tag close 60 | else if (match === "/>") { 61 | waitingStartTag = true; 62 | } 63 | // End tag 64 | else if (match[0] === "<" && match[1] === "/") { 65 | skipList.push(match.slice(2)); 66 | } 67 | // This should be a start tag 68 | else if (match[0] === "<" && waitingStartTag) { 69 | waitingStartTag = false; 70 | } else if (match[0] == "<") { 71 | tagName = match.slice(1); 72 | // Omit XML definition. 73 | if (tagName === "?xml") { 74 | continue; 75 | } 76 | 77 | let idx = skipList.lastIndexOf(tagName); 78 | if (idx != -1) { 79 | skipList.splice(idx, 1); 80 | } else { 81 | xpath.push(tagName); 82 | } 83 | } 84 | }; 85 | } 86 | 87 | // Get next line 88 | if (row >= 0) { 89 | line = document.lineAt(row).text; 90 | } 91 | } 92 | 93 | return xpath.reverse(); 94 | } 95 | 96 | 97 | 98 | export function textBeforeWordEquals(document: TextDocument, position: Position, textToMatch: string) { 99 | const wordRange = document.getWordRangeAtPosition(position); 100 | const wordStart = wordRange ? wordRange.start : position; 101 | if (wordStart.character < textToMatch.length) { 102 | // Not enough room to match 103 | return false; 104 | } 105 | 106 | const charBeforeWord = document.getText(new Range(new Position(wordStart.line, wordStart.character - textToMatch.length), wordStart)); 107 | return charBeforeWord === textToMatch; 108 | } 109 | 110 | export function isTagName(document: TextDocument, position: Position): boolean { 111 | return textBeforeWordEquals(document, position, '<'); 112 | } 113 | 114 | export function isClosingTagName(document: TextDocument, position: Position): boolean { 115 | return textBeforeWordEquals(document, position, ' document.lineAt(wordEnd.line).text.length - 1) { 133 | return false; 134 | } 135 | 136 | // TODO: This detection is very limited, only if the char before the word is ' or " 137 | const rangeBefore = new Range(wordStart.line, wordStart.character - 1, wordStart.line, wordStart.character); 138 | if (document.getText(rangeBefore).match(/'|"/)) { 139 | return true; 140 | } 141 | 142 | const word = document.getText(wordRange); 143 | if (word && /\b\w+\b/.exec(word)) { 144 | return true; 145 | } 146 | 147 | return false; 148 | } 149 | 150 | export function isAttribute(document: TextDocument, position: Position): boolean { 151 | const lineContent = document.lineAt(position).text; 152 | const wordRange = document.getWordRangeAtPosition(position); 153 | const wordStart = wordRange ? wordRange.start : position; 154 | const text = document.getText(); 155 | const offset = document.offsetAt(wordStart); 156 | const quotsBeforeCount = lineContent.substring(position.character, 0).split('"').length - 1; 157 | return quotsBeforeCount % 2 === 0 && 158 | (text.lastIndexOf('<', offset) > text.lastIndexOf('>', offset) && 159 | text.lastIndexOf(' ', offset) > text.lastIndexOf('<', offset) || 160 | text[offset] === '>' && text.lastIndexOf('<', offset) > text.lastIndexOf('>', offset - 1)); 161 | } 162 | -------------------------------------------------------------------------------- /src/models/config.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export interface ConfigHandlerAndPropertyModel { 4 | handler: string; 5 | targetProperty: string; 6 | value?: string; 7 | } 8 | 9 | export interface ConfigWrapper { 10 | widget: string; 11 | properties: ConfigHandlerAndPropertyModel[]; 12 | defaults?: { [name: string]: string }; 13 | priority?: number; 14 | } 15 | 16 | export interface ConfigValueTransformer { 17 | properties: string[]; 18 | type: 'enum' | 'color' | 'edgeInsets'; 19 | enumType?: string; 20 | } 21 | 22 | export interface Config { 23 | /** 24 | * `[ { widget: "Container", properties: [ { handler: ":width", targetProperty: "width" } ] } ]` 25 | */ 26 | wrappers?: ConfigWrapper[]; 27 | 28 | /** 29 | * `[ { widget: "Padding", properties: [ { handler: ":padding", targetProperty: "padding" } ] } ]` 30 | */ 31 | childWrappers?: ConfigWrapper[]; 32 | 33 | /** 34 | * `[ { properties: ["alignment"], type: "enum", data: "Alignment" } ]` 35 | */ 36 | valueTransformers?: ConfigValueTransformer[]; 37 | 38 | /** 39 | * `{ "Text": "text", "Icon": "icon", "Image": "source" }` 40 | */ 41 | unnamedProperties?: { [name: string]: string }; 42 | 43 | /** 44 | * `{ "DropdownButton": ["items", "children"] }` 45 | */ 46 | arrayProperties?: { [name: string]: string[] }; 47 | 48 | /** 49 | * `["items"]` 50 | */ 51 | controlsWithTextEditingControllers?: string[]; 52 | } 53 | -------------------------------------------------------------------------------- /src/models/models.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface RootWidgetModel { 3 | // name: string; 4 | stateful: boolean; 5 | type: string; 6 | controller: string; 7 | controllerPath: string; 8 | rootChild: WidgetModel; 9 | params: ParamModel[]; 10 | mixins: string[]; 11 | providers: VariableModel[]; 12 | vars: VariableModel[]; 13 | imports: ImportModel[]; 14 | routeAware: boolean; 15 | } 16 | 17 | export interface ImportModel { 18 | // name: string; 19 | path: string; 20 | } 21 | 22 | export interface ParamModel { 23 | name: string; 24 | type: string; 25 | value: string; 26 | required: boolean; 27 | superParamName: string; 28 | } 29 | 30 | export interface WidgetModel { 31 | type: string; 32 | 33 | controllers: VariableModel[]; 34 | vars: VariableModel[]; 35 | formControls: FormControlModel[]; 36 | 37 | properties: PropertyModel[]; 38 | 39 | wrappedWidgets: WidgetModel[]; 40 | 41 | mixins?: string[]; 42 | comments?: string[]; 43 | 44 | /** 45 | * Determines whether the element is a property of the parent widget or not. 46 | * e.g. title in AppBar is a PropertyElement: ... <title> <AppBar> 47 | */ 48 | isPropertyElement?: boolean; 49 | 50 | /** 51 | * Called when the widget and its properties have been resolved. 52 | */ 53 | onResolved: ((widget: WidgetModel) => void)[]; 54 | 55 | /** 56 | * tempDate used only to transfer data from resolve() to generate() in the PropertyHandler. 57 | */ 58 | tempData?: any; 59 | 60 | id?: any; 61 | 62 | /** 63 | * Determines whether the widget is created by a property handler or not. 64 | */ 65 | isCustom?: boolean; 66 | } 67 | 68 | export interface VariableModel { 69 | name: string; 70 | type: string; 71 | isPrivate?: boolean; 72 | value?: string; 73 | skipGenerate?: boolean; 74 | } 75 | 76 | export interface FormControlModel { 77 | name: string; 78 | type: string; 79 | controller: string; 80 | } 81 | 82 | export interface PropertyModel { 83 | name: string; 84 | value: string | WidgetModel | WidgetModel[]; 85 | dataType: 'string' | 'object' | 'widget' | 'widgetList' | 'function' | 'propertyElement'; 86 | skipGeneratingCode?: boolean; 87 | controller?: VariableModel; 88 | // isEvent?: boolean; 89 | // isBound?: boolean; 90 | extraData?: ExtraDataModel | null; 91 | generateCodeDelegate?: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string; 92 | } 93 | 94 | export interface ExtraDataModel { 95 | //widget?: WidgetModel; 96 | parameters?: { name: string, type: string }[]; 97 | // variables?: { name: string, type: string, value: string }[]; 98 | [name: string]: any; 99 | } 100 | 101 | export interface AttributeModel { 102 | name: string; 103 | value: string; 104 | // isEvent?: boolean; 105 | // isBound?: boolean; 106 | } 107 | 108 | export interface AttributeInfo { 109 | name: string; 110 | snippet?: string; 111 | } -------------------------------------------------------------------------------- /src/parser/syntax.ts: -------------------------------------------------------------------------------- 1 | // 'use strict'; 2 | 3 | // To improve readability, the regular expression patterns in this file are 4 | // written as tagged template literals. The `regex` tag function strips literal 5 | // whitespace characters and line comments beginning with `//` and returns a 6 | // RegExp instance. 7 | // 8 | // Escape sequences are preserved as-is in the resulting regex, so 9 | // double-escaping isn't necessary. A pattern may embed another pattern using 10 | // `${}` interpolation. 11 | 12 | // -- Common Symbols ----------------------------------------------------------- 13 | 14 | const syntax: any = {}; 15 | 16 | syntax.Char = regex` 17 | (?: 18 | [ 19 | \t 20 | \n 21 | \r 22 | \x20-\uD7FF 23 | \uE000-\uFFFD 24 | ] 25 | | 26 | [\uD800-\uDBFF][\uDC00-\uDFFF] 27 | ) 28 | `; 29 | 30 | // Partial implementation. 31 | // 32 | // To be compliant, the matched text must result in an error if it contains the 33 | // string `]]>`, but that can't be easily represented here so we do it in the 34 | // parser. 35 | syntax.CharData = regex` 36 | [^<&]+ 37 | `; 38 | 39 | syntax.NameStartChar = regex` 40 | (?: 41 | [ 42 | : 43 | A-Z 44 | _ 45 | a-z 46 | \xC0-\xD6 47 | \xD8-\xF6 48 | \xF8-\u02FF 49 | \u0370-\u037D 50 | \u037F-\u1FFF 51 | \u200C-\u200D 52 | \u2070-\u218F 53 | \u2C00-\u2FEF 54 | \u3001-\uD7FF 55 | \uF900-\uFDCF 56 | \uFDF0-\uFFFD 57 | ] 58 | | 59 | [\uD800-\uDB7F][\uDC00-\uDFFF] 60 | ) 61 | `; 62 | 63 | syntax.NameChar = regex` 64 | (?: 65 | ${syntax.NameStartChar} 66 | | 67 | [ 68 | . 69 | 0-9 70 | \xB7 71 | \u0300-\u036F 72 | \u203F-\u2040 73 | - 74 | ] 75 | ) 76 | `; 77 | 78 | syntax.Name = regex` 79 | ${syntax.NameStartChar} 80 | (?:${syntax.NameChar})* 81 | `; 82 | 83 | // Loose implementation. The entity will be validated in the `replaceReference` 84 | // function. 85 | syntax.Reference = regex` 86 | &\S+?; 87 | `; 88 | 89 | syntax.S = regex` 90 | [\x20\t\r\n]+ 91 | `; 92 | 93 | // -- Attributes --------------------------------------------------------------- 94 | syntax.Eq = regex` 95 | (?:${syntax.S})? 96 | = 97 | (?:${syntax.S})? 98 | `; 99 | 100 | syntax.AttributeName = regex` 101 | (?: 102 | ${syntax.NameStartChar} 103 | (?:${syntax.NameChar})* 104 | | 105 | \[ 106 | ${syntax.NameStartChar} 107 | (?:${syntax.NameChar})* 108 | \] 109 | | 110 | \( 111 | ${syntax.NameStartChar} 112 | (?:${syntax.NameChar})* 113 | \) 114 | ) 115 | `; 116 | 117 | syntax.Attribute = regex` 118 | ${syntax.AttributeName} 119 | (?:${syntax.Eq})* 120 | (?: 121 | "(?: 122 | [^"] | ${syntax.Reference} 123 | )*" 124 | | 125 | '(?: 126 | [^'] | ${syntax.Reference} 127 | )*' 128 | )* 129 | `; 130 | 131 | // syntax.Attribute = regex` 132 | // ${syntax.AttributeName} 133 | // (?:${syntax.Eq})* 134 | // (?: 135 | // "(?: 136 | // [^<&"] | ${syntax.Reference} 137 | // )*" 138 | // | 139 | // '(?: 140 | // [^<&'] | ${syntax.Reference} 141 | // )*' 142 | // )* 143 | // `; 144 | 145 | syntax.EventAttribute = regex` 146 | \( 147 | // group 1 148 | ( 149 | ${syntax.NameStartChar} 150 | (?:${syntax.NameChar})* 151 | ) 152 | \)`; 153 | 154 | syntax.BoundAttribute = regex` 155 | \[ 156 | // group 1 157 | ( 158 | ${syntax.NameStartChar} 159 | (?:${syntax.NameChar})* 160 | ) 161 | \]`; 162 | 163 | syntax.Pipes = regex`[^"\|]+|("[^"]*")`; 164 | syntax.PipeArgs = regex`[^":]+|("[^"]*")`; 165 | 166 | // -- Elements ----------------------------------------------------------------- 167 | syntax.CDSect = regex` 168 | <!\[CDATA\[ 169 | // Group 1: CData text content (optional) 170 | ( 171 | (?:${syntax.Char})*? 172 | ) 173 | \]\]> 174 | `; 175 | 176 | syntax.EmptyElemTag = regex` 177 | < 178 | // Group 1: Element name 179 | (${syntax.Name}) 180 | // Group 2: Attributes (optional) 181 | ( 182 | (?: 183 | ${syntax.S} 184 | ${syntax.Attribute} 185 | )* 186 | ) 187 | (?:${syntax.S})? 188 | /> 189 | `; 190 | 191 | syntax.ETag = regex` 192 | </ 193 | // Group 1: End tag name 194 | (${syntax.Name}) 195 | (?:${syntax.S})? 196 | > 197 | `; 198 | 199 | syntax.STag = regex` 200 | < 201 | // Group 1: Start tag name 202 | (${syntax.Name}) 203 | // Group 2: Attributes (optional) 204 | ( 205 | (?: 206 | ${syntax.S} 207 | ${syntax.Attribute} 208 | )* 209 | ) 210 | (?:${syntax.S})? 211 | > 212 | `; 213 | 214 | // -- Misc --------------------------------------------------------------------- 215 | 216 | // Special pattern that matches an entire string consisting only of `Char` 217 | // characters. 218 | syntax.CharOnly = regex` 219 | ^(?:${syntax.Char})*$ 220 | `; 221 | 222 | syntax.Comment = regex` 223 | <!-- 224 | // Group 1: Comment text (optional) 225 | ( 226 | (?: 227 | (?!-) ${syntax.Char} 228 | | - (?!-) ${syntax.Char} 229 | )* 230 | ) 231 | --> 232 | `; 233 | 234 | // Loose implementation since doctype declarations are discarded. 235 | // 236 | // It's not possible to fully parse a doctype declaration with a regex, but 237 | // since we just discard them we can skip parsing the fiddly inner bits and use 238 | // a regex to speed things up. 239 | syntax.doctypedecl = regex` 240 | <!DOCTYPE 241 | ${syntax.S} 242 | [^[>]* 243 | (?: 244 | \[ [\s\S]+? \] 245 | (?:${syntax.S})? 246 | )? 247 | > 248 | `; 249 | 250 | // Loose implementation since processing instructions are discarded. 251 | syntax.PI = regex` 252 | <\? 253 | // Group 1: PITarget 254 | ( 255 | ${syntax.Name} 256 | ) 257 | (?: 258 | ${syntax.S} 259 | (?:${syntax.Char})*? 260 | )? 261 | \?> 262 | `; 263 | 264 | // Loose implementation since XML declarations are discarded. 265 | syntax.XMLDecl = regex` 266 | <\?xml 267 | ${syntax.S} 268 | [\s\S]+? 269 | \?> 270 | `; 271 | 272 | // -- Helpers ------------------------------------------------------------------ 273 | syntax.Anchored = {}; 274 | syntax.Global = {}; 275 | 276 | // Create anchored and global variations of each pattern. 277 | Object.keys(syntax).forEach(name => { 278 | if (name !== 'Anchored' && name !== 'CharOnly' && name !== 'Global') { 279 | let pattern = syntax[name]; 280 | 281 | syntax.Anchored[name] = new RegExp('^' + pattern.source); 282 | syntax.Global[name] = new RegExp(pattern.source, 'g'); 283 | } 284 | }); 285 | 286 | function regex(strings: any, ...embeddedPatterns: { source: string; }[]) { 287 | let { length, raw } = strings; 288 | let lastIndex = length - 1; 289 | let pattern = ''; 290 | 291 | for (let i = 0; i < length; ++i) { 292 | pattern += raw[i] 293 | .replace(/(^|[^\\])\/\/.*$/gm, '$1') // remove end-of-line comments 294 | .replace(/\s+/g, ''); // remove all whitespace 295 | 296 | if (i < lastIndex) { 297 | pattern += embeddedPatterns[i].source; 298 | } 299 | } 300 | 301 | return new RegExp(pattern); 302 | } 303 | 304 | export = syntax; -------------------------------------------------------------------------------- /src/parser/types.ts: -------------------------------------------------------------------------------- 1 | 2 | declare namespace parseXml { 3 | 4 | interface NodeBase { 5 | parent?: NodeBase; 6 | type: string; 7 | } 8 | 9 | interface Document extends NodeBase { 10 | type: "document"; 11 | children: NodeBase[]; 12 | } 13 | 14 | interface CData extends NodeBase { 15 | type: "cdata"; 16 | text: string; 17 | } 18 | 19 | interface Comment extends NodeBase { 20 | type: "comment"; 21 | content: string; 22 | } 23 | 24 | interface Text extends NodeBase { 25 | type: "text"; 26 | text: string; 27 | } 28 | 29 | interface Element extends NodeBase { 30 | type: "element"; 31 | attributes: { [key: string]: string }; 32 | children: NodeBase[]; 33 | name: string; 34 | preserveWhitespace?: string; 35 | } 36 | 37 | type Node = CData | Comment | Element | Text; 38 | 39 | interface ParseOptions { 40 | ignoreUndefinedEntities?: boolean; 41 | preserveCdata?: boolean; 42 | preserveComments?: boolean; 43 | resolveUndefinedEntity?: (ref: string) => string; 44 | } 45 | } 46 | export = parseXml; 47 | -------------------------------------------------------------------------------- /src/property-handlers/child-builder.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Child Builder Tests", function () { 4 | 5 | test("ListView basic", function() { 6 | const xml = ` 7 | <ListView :childBuilder="item of component.items"> 8 | <Text text="item.title" /> 9 | </ListView> 10 | `; 11 | 12 | const expected = ` 13 | ListView( 14 | children: WidgetHelpers.mapToWidgetList(component.items, (item, index) { 15 | return Text( 16 | item.title, 17 | ); 18 | }), 19 | ) 20 | `; 21 | 22 | const generated = generateWidget(xml); 23 | assertEqual(generated, expected); 24 | }); 25 | 26 | test("ListView with stream", function() { 27 | const xml = ` 28 | <ListView :childBuilder="item of component.items | stream"> 29 | <Text text="item.title" /> 30 | </ListView> 31 | `; 32 | 33 | const expected = ` 34 | StreamBuilder( 35 | initialData: null, 36 | stream: component.items, 37 | builder: (BuildContext context, componentItemsSnapshot) { 38 | final componentItemsValue = componentItemsSnapshot.data; 39 | if (componentItemsValue == null) { 40 | return Container(width: 0, height: 0); 41 | } 42 | return ListView( 43 | children: WidgetHelpers.mapToWidgetList(componentItemsValue, (item, index) { 44 | return Text( 45 | item.title, 46 | ); 47 | }), 48 | ); 49 | }, 50 | ) 51 | `; 52 | 53 | const generated = generateWidget(xml); 54 | assertEqual(generated, expected); 55 | }); 56 | 57 | 58 | // test("ListView itemBuilder with items", function() { 59 | // const xml = ` 60 | // <ListView :use="builder"> 61 | // <builder name="itemBuilder" data="index, item of items" params="context, index"> 62 | // <PopupMenuItem> 63 | // <Text text="text" /> 64 | // </PopupMenuItem> 65 | // </builder> 66 | // </ListView> 67 | // `; 68 | 69 | // const expected = ` 70 | // ListView.builder( 71 | // itemBuilder: (context, index) { 72 | // if (items.length == 0) { 73 | // return null; 74 | // } 75 | 76 | // final item = items[index]; 77 | // return PopupMenuItem( 78 | // child: Text( 79 | // "text" 80 | // ) 81 | // ); 82 | // } 83 | // ) 84 | // `; 85 | 86 | // const generated = generateWidget(xml); 87 | // assertEqual(generated, expected); 88 | // }); 89 | 90 | 91 | // test("ListView itemBuilder with items and stream", function() { 92 | // const xml = ` 93 | // <ListView :use="builder"> 94 | // <builder name="itemBuilder" data="index, item of itemsStream | stream" params="context, index"> 95 | // <PopupMenuItem> 96 | // <Text text="text" /> 97 | // </PopupMenuItem> 98 | // </builder> 99 | // </ListView> 100 | // `; 101 | 102 | // const expected = ` 103 | // StreamBuilder( 104 | // builder: (BuildContext context, itemsStreamSnapshot) { 105 | // final itemsStreamValue = itemsStreamSnapshot.data; 106 | // if (itemsStreamValue == null) { 107 | // return Container(width: 0, height: 0); 108 | // } 109 | // return ListView.builder( 110 | // itemBuilder: (context, index) { 111 | // if (itemsStreamValue.length == 0) { 112 | // return null; 113 | // } 114 | 115 | // final item = itemsStreamValue[index]; 116 | // return PopupMenuItem( 117 | // child: Text( 118 | // "text" 119 | // ) 120 | // ); 121 | // } 122 | // ); 123 | // }, 124 | // initialData: null, 125 | // stream: itemsStream 126 | // ) 127 | // `; 128 | 129 | // const generated = generateWidget(xml); 130 | // assertEqual(generated, expected); 131 | // }); 132 | }); -------------------------------------------------------------------------------- /src/property-handlers/child-builder.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, ExtraDataModel, AttributeModel, PropertyModel } from '../models/models'; 4 | import { extractForLoopParams, makeTabs, sortProperties, spaceAfter } from "../utils"; 5 | import { PropertyResolver } from "../resolvers/property-resolver"; 6 | 7 | export class ChildBuilderHandler extends CustomPropertyHandler { 8 | priority = -100000; // lowest priority 9 | valueSnippet = '"item of ${0:items}"'; 10 | 11 | constructor(private readonly propertyResolver: PropertyResolver) { 12 | super(); 13 | } 14 | 15 | 16 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 17 | return true; 18 | } 19 | 20 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 21 | let wrapperWidget: WidgetModel | null = null; 22 | let extraData: ExtraDataModel | null = null; 23 | 24 | const { listName, indexName, itemName, typeName } = extractForLoopParams(attr.value); 25 | const tempData: any = { listName, indexName, itemName, typeName }; 26 | const listNameWithPipes = attr.value.substr(attr.value.indexOf(listName)); 27 | const contentWidget: WidgetModel = { 28 | controllers: [], 29 | vars: [], 30 | formControls: [], 31 | properties: [ 32 | { 33 | dataType: 'function', 34 | name: ':childBuilder', 35 | value: '' 36 | } 37 | ], 38 | type: widget.type === 'builder' ? 'YouShouldNotSetChildBuilderPropertyInBuilderTag' : widget.type, 39 | tempData: tempData, 40 | wrappedWidgets: [widget], 41 | onResolved: [] 42 | }; 43 | 44 | const result = this.propertyResolver.pipeValueResolver.resolve(element, attr.name, listNameWithPipes, contentWidget); 45 | wrapperWidget = result.wrapperWidget || contentWidget; 46 | tempData.listValueVariableName = result.wrapperWidget ? result.value : tempData.listName; 47 | 48 | return { extraData, wrapperWidget, value: result.value, handled: true }; 49 | } 50 | 51 | canGenerate(widget: WidgetModel): boolean { 52 | return true; 53 | } 54 | 55 | generate(widget: WidgetModel, tabsLevel: number, 56 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 57 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 58 | const tabs = makeTabs(tabsLevel); 59 | const data = widget.tempData; 60 | const originalWidget = widget.wrappedWidgets[0]; 61 | const indexName = data.indexName || 'index'; 62 | let widgetProp = originalWidget.properties.filter(a => a.dataType === 'widget')[0]; 63 | let code = ''; 64 | 65 | // generate other user-defined properties 66 | if (!originalWidget.isPropertyElement) { 67 | originalWidget.properties.sort(sortProperties) 68 | .filter(a => !a.skipGeneratingCode && ['children'].indexOf(a.name) === -1 && a !== widgetProp) 69 | .forEach(p => { 70 | code += generatePropertyCode(originalWidget as WidgetModel, p, tabsLevel) + `,\n`; 71 | }); 72 | } 73 | 74 | // get child widget from properties 75 | let childWidget: WidgetModel = widgetProp ? widgetProp.value as WidgetModel : null as any; 76 | if (!childWidget) { 77 | const widgetProp = originalWidget.properties.filter(a => a.dataType === 'widgetList')[0]; 78 | childWidget = widgetProp && widgetProp.value ? (widgetProp.value as WidgetModel[])[0] : null as any; 79 | } 80 | 81 | code += 82 | `${tabs}children: WidgetHelpers.mapToWidgetList(${data.listValueVariableName}, (${spaceAfter(data.typeName)}${data.itemName || 'item'}, ${data.indexName || 'index'}) { 83 | ${tabs} return ${generateChildWidgetCode(childWidget, tabsLevel + 1)}; 84 | ${tabs}})`; 85 | 86 | return code; 87 | } 88 | } -------------------------------------------------------------------------------- /src/property-handlers/child-wrapper-property.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Child Wrapper Properties Tests", function () { 4 | 5 | test("basic", function() { 6 | const xml = `<Container :padding="4"> 7 | <Text text="'hello!'" /> 8 | </Container>`; 9 | 10 | const expected = ` 11 | Container( 12 | child: Padding( 13 | padding: const EdgeInsets.all(4), 14 | child: Text( 15 | 'hello!', 16 | ), 17 | ), 18 | )`; 19 | 20 | const generated = generateWidget(xml); 21 | assertEqual(generated, expected); 22 | }); 23 | 24 | 25 | test("with stream", function() { 26 | const xml = `<Container :padding="paddingStream | stream"> 27 | <Text text="'hello!'" /> 28 | </Container>`; 29 | 30 | const expected = ` 31 | StreamBuilder( 32 | initialData: null, 33 | stream: paddingStream, 34 | builder: (BuildContext context, paddingStreamSnapshot) { 35 | final paddingStreamValue = paddingStreamSnapshot.data; 36 | if (paddingStreamValue == null) { 37 | return Container(width: 0, height: 0); 38 | } 39 | return Container( 40 | child: Padding( 41 | padding: paddingStreamValue, 42 | child: Text( 43 | 'hello!', 44 | ), 45 | ), 46 | ); 47 | }, 48 | ) 49 | `; 50 | 51 | const generated = generateWidget(xml); 52 | assertEqual(generated, expected); 53 | }); 54 | 55 | test("test 2", function() { 56 | const xml = ` 57 | <RaisedButton :text="'hello!'"> 58 | </RaisedButton> 59 | `; 60 | 61 | const expected = ` 62 | RaisedButton( 63 | child: Text( 64 | 'hello!', 65 | ), 66 | )`; 67 | 68 | const generated = generateWidget(xml); 69 | assertEqual(generated, expected); 70 | }); 71 | 72 | test("with pipes", function() { 73 | const xml = ` 74 | <RaisedButton :text="'hello' | translate"> 75 | </RaisedButton> 76 | `; 77 | 78 | const expected = ` 79 | RaisedButton( 80 | child: Text( 81 | _pipeProvider.transform(context, "translate", 'hello', []), 82 | ), 83 | )`; 84 | 85 | const generated = generateWidget(xml); 86 | assertEqual(generated, expected); 87 | }); 88 | }); -------------------------------------------------------------------------------- /src/property-handlers/child-wrapper-property.ts: -------------------------------------------------------------------------------- 1 | import { PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, AttributeModel, PropertyModel } from '../models/models'; 4 | import { ConfigHandlerAndPropertyModel } from '../models/config'; 5 | import { WrapperPropertyHandler } from "./wrapper-property"; 6 | import { ValueTransformersProvider } from "../providers/value-transformers-provider"; 7 | import { PropertyResolver } from "../resolvers/property-resolver"; 8 | 9 | export class ChildWrapperPropertyHandler extends WrapperPropertyHandler { 10 | constructor(propertyResolver: PropertyResolver, properties: ConfigHandlerAndPropertyModel[], widgetType: string, 11 | defaults?: { [name: string]: string }, priority: number = 100) { 12 | super(propertyResolver, properties, widgetType, defaults, priority); 13 | } 14 | 15 | getRelatedProperties(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): string[] { 16 | return this.properties.map(a => a.handler); 17 | } 18 | 19 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 20 | const property = this.getProperty(attr.name); 21 | attr.value = property.value !== null && property.value !== undefined ? property.value : attr.value; 22 | let value = property.targetProperty ? ValueTransformersProvider.transform(attr.value, property.targetProperty, this.targetWidgetType) : attr.value; 23 | 24 | const propertyName = this.propertyResolver.isUnNamedParameter(property.targetProperty, this.targetWidgetType) ? '' : property.targetProperty; 25 | 26 | const newChildWidget: WidgetModel = { 27 | controllers: [], 28 | vars: [], 29 | formControls: [], 30 | properties: [], 31 | type: this.targetWidgetType, 32 | wrappedWidgets: [], 33 | onResolved: [] 34 | }; 35 | 36 | // if the widget was a wrapper for the original widget get the original one. 37 | if (widget.wrappedWidgets) { 38 | const wrappedWidget = widget.wrappedWidgets[0]; 39 | if (wrappedWidget) { 40 | widget = wrappedWidget; 41 | } 42 | } 43 | 44 | const onResolve = (widget: WidgetModel) => { 45 | let child = widget.properties.filter(a => a.name === 'child')[0]; 46 | if (!child) { 47 | child = { 48 | name: 'child', 49 | dataType: 'widget', 50 | value: null as any 51 | }; 52 | widget.properties.push(child); 53 | } 54 | 55 | newChildWidget.properties = [ 56 | { 57 | dataType: 'object', name: propertyName, value: value 58 | }, 59 | 60 | // add related properties 61 | ... this.createRelatedProperties(element.attributes, property.targetProperty), 62 | 63 | // add properties' default values 64 | ... this.createPropertiesDefaultValues() 65 | ]; 66 | 67 | if (child.value) { 68 | newChildWidget.properties.push({ 69 | dataType: 'widget', name: 'child', value: child.value 70 | }); 71 | } 72 | 73 | // todo apply binding on related properties 74 | // for (const prop of relatedProperties) { 75 | // const resolveResult = this.pipeValueResolver.resolve(element, prop.name, prop.value as any, wrapperWidget, true); 76 | // wrapperWidget = resolveResult.wrapperWidget || wrapperWidget; 77 | // prop.value = resolveResult.value; 78 | // } 79 | 80 | child.value = newChildWidget as any; 81 | }; 82 | widget.onResolved.push(onResolve); 83 | 84 | const result = this.propertyResolver.pipeValueResolver.resolve(element, attr.name, attr.value, widget, true); 85 | const wrapperWidget = result.wrapperWidget; 86 | if (attr.value !== result.value) { 87 | value = result.value; 88 | } 89 | 90 | return { extraData: null, wrapperWidget, value: attr.value, handled: true }; 91 | } 92 | } -------------------------------------------------------------------------------- /src/property-handlers/form-control.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, ExtraDataModel, AttributeModel } from '../models/models'; 4 | import { FormGroupHandler } from "./form-group"; 5 | import { PropertyResolver } from "../resolvers/property-resolver"; 6 | import { findWidgetByName } from "../utils"; 7 | 8 | export class FormControlHandler extends CustomPropertyHandler { 9 | priority = 8000; // less than :disable 10 | 11 | constructor(private readonly propertyResolver: PropertyResolver) { 12 | super(); 13 | } 14 | 15 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 16 | return true; 17 | } 18 | 19 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 20 | let wrapperWidget: WidgetModel | null = null; 21 | let extraData: ExtraDataModel | null = null; 22 | 23 | const targetWidget = findWidgetByName(element.name, widget); 24 | const formGroupName = FormGroupHandler.getFormGroup(element) || 'formGroup'; 25 | targetWidget.vars.push({ 26 | name: formGroupName, 27 | type: 'FormGroup' 28 | }); 29 | 30 | const name = attr.value; 31 | const formControlName = `${formGroupName}.get(${name})`; 32 | // const formControlName = `${attr.value}FormControl`; 33 | // targetWidget.vars.push({ 34 | // name: `FormControl get ${attr.value}FormControl => ${formGroupName}.get('${attr.value}');`, 35 | // type: '' 36 | // }); 37 | 38 | let addLocalVar = true; 39 | const controlsWithTextEditingControllers = ['TextField', 'TextFormField', 'CupertinoTextField', ...(this.propertyResolver.getConfig().controlsWithTextEditingControllers || [])]; 40 | if (controlsWithTextEditingControllers.filter(a => a === targetWidget.type).length === 1) { 41 | addLocalVar = false; 42 | const controllerName = element.attributes['controller'] ? element.attributes['controller'].split(' ')[1] : ''; 43 | const privateControllerName = `ctrl._attachController(ctrl.${formGroupName}, ${name}, ${controllerName || '() => TextEditingController()'})`; 44 | 45 | if (!controllerName) { 46 | // only add controller if there is no one present 47 | targetWidget.properties.push({ 48 | dataType: 'object', 49 | // controller: { 50 | // name: privateControllerName, 51 | // type: 'TextEditingController', 52 | // isPrivate: true 53 | // }, 54 | value: privateControllerName, 55 | name: 'controller' 56 | }); 57 | } 58 | targetWidget.formControls.push({ 59 | name: formControlName, 60 | controller: controllerName || privateControllerName, 61 | type: '' 62 | }); 63 | } 64 | else { 65 | targetWidget.properties.push({ 66 | dataType: 'object', 67 | name: 'value', 68 | value: `${attr.value}Snapshot.data` 69 | }); 70 | targetWidget.properties.push({ 71 | dataType: 'object', 72 | name: 'onChanged', 73 | value: `(value) => ctrl.${formControlName}.value = value` 74 | }); 75 | } 76 | 77 | const streamBuilder = this.propertyResolver.pipeValueResolver.createStreamBuilder( 78 | `ctrl.${formControlName}.valueStream`, 79 | `ctrl.${formControlName}.value`, 80 | '', 81 | widget, attr.value, true, false, addLocalVar); 82 | 83 | attr.value = streamBuilder.value; 84 | wrapperWidget = streamBuilder.wrapperWidget; 85 | 86 | const valueProp = targetWidget.properties.filter(a => a.name === 'value')[0]; 87 | if (valueProp) { 88 | valueProp.value = attr.value; 89 | } 90 | 91 | return { extraData, wrapperWidget, value: attr.value, handled: true }; 92 | } 93 | } -------------------------------------------------------------------------------- /src/property-handlers/form-group.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, AttributeModel, PropertyModel } from '../models/models'; 4 | 5 | export class FormGroupHandler extends CustomPropertyHandler { 6 | 7 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 8 | return true; 9 | } 10 | 11 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 12 | return { 13 | extraData: null, 14 | wrapperWidget: widget, 15 | value: attr.value, 16 | handled: false // keep it as property to be used in SwitchResolver 17 | }; 18 | } 19 | 20 | canGenerate(widget: WidgetModel): boolean { 21 | return true; 22 | } 23 | 24 | generate(widget: WidgetModel, tabsLevel: number, 25 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 26 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 27 | return ''; // don't generate code for this property as it is custom 28 | } 29 | 30 | static getFormGroup(element: parseXml.Element): string { 31 | if (!element || !element.attributes) { 32 | return ''; 33 | } 34 | 35 | if (':formGroup' in element.attributes) { 36 | return element.attributes[':formGroup'] || ''; 37 | } 38 | 39 | return this.getFormGroup(element.parent as any); 40 | } 41 | } -------------------------------------------------------------------------------- /src/property-handlers/form-submit.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, AttributeModel } from '../models/models'; 4 | import { WrapperDisablePropertyHandler } from "./wrapper-disable-property"; 5 | import { FormGroupHandler } from "./form-group"; 6 | import { PropertyResolver } from "../resolvers/property-resolver"; 7 | import { findWidgetByName } from "../utils"; 8 | 9 | export class FormSubmitHandler extends CustomPropertyHandler { 10 | priority = 9000 - 1; // less than Disable 11 | 12 | constructor(private readonly propertyResolver: PropertyResolver) { 13 | super(); 14 | } 15 | 16 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 17 | return true; 18 | } 19 | 20 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 21 | const formGroupName = 'ctrl.' + (attr.value || FormGroupHandler.getFormGroup(element) || 'formGroup'); 22 | const disableWidget = new WrapperDisablePropertyHandler(this.propertyResolver); 23 | const resolveResult = disableWidget.resolve( 24 | element, { 25 | name: ':disable', 26 | value: `!(${formGroupName}.submitEnabledStream | stream:${formGroupName}.submitEnabled)` 27 | }, 28 | widget 29 | ); 30 | 31 | const targetWidget = findWidgetByName(element.name, widget); 32 | targetWidget.properties.push({ 33 | dataType: 'object', name: 'onPressed', value: `${formGroupName}.submit` 34 | }); 35 | 36 | return resolveResult; 37 | } 38 | } -------------------------------------------------------------------------------- /src/property-handlers/form.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Forms", function () { 4 | 5 | test(":formSubmit", function() { 6 | const xml = ` 7 | <ProgressButton :formSubmit="loginFormGroup"> 8 | <Text text="'Login'" /> 9 | </ProgressButton> 10 | `; 11 | 12 | const expected = ` 13 | StreamBuilder( 14 | initialData: ctrl.loginFormGroup.submitEnabled, 15 | stream: ctrl.loginFormGroup.submitEnabledStream, 16 | builder: (BuildContext context, ctrlLoginFormGroupSubmitEnabledStreamSnapshot) { 17 | final ctrlLoginFormGroupSubmitEnabledStreamValue = ctrlLoginFormGroupSubmitEnabledStreamSnapshot.data; 18 | return Disable( 19 | event: ctrl.loginFormGroup.submit, 20 | value: !(ctrlLoginFormGroupSubmitEnabledStreamValue), 21 | builder: (BuildContext context, event) { 22 | return ProgressButton( 23 | onPressed: event, 24 | child: Text( 25 | 'Login', 26 | ), 27 | ); 28 | }, 29 | ); 30 | }, 31 | ) 32 | `; 33 | 34 | const generated = generateWidget(xml); 35 | assertEqual(generated, expected); 36 | }); 37 | 38 | test(":formSubmit with another stream & :margin", function() { 39 | const xml = ` 40 | <ProgressButton :margin="0" :formSubmit="loginFormGroup" 41 | buttonState="ctrl.statusStream | behavior"> 42 | <Text text="'Login'" /> 43 | </ProgressButton> 44 | `; 45 | 46 | const expected = ` 47 | StreamBuilder( 48 | initialData: ctrl.statusStream.value, 49 | stream: ctrl.statusStream, 50 | builder: (BuildContext context, ctrlStatusStreamSnapshot) { 51 | final ctrlStatusStreamValue = ctrlStatusStreamSnapshot.data; 52 | if (ctrlStatusStreamValue == null) { 53 | return Container(width: 0, height: 0); 54 | } 55 | return StreamBuilder( 56 | initialData: ctrl.loginFormGroup.submitEnabled, 57 | stream: ctrl.loginFormGroup.submitEnabledStream, 58 | builder: (BuildContext context, ctrlLoginFormGroupSubmitEnabledStreamSnapshot) { 59 | final ctrlLoginFormGroupSubmitEnabledStreamValue = ctrlLoginFormGroupSubmitEnabledStreamSnapshot.data; 60 | return Disable( 61 | event: ctrl.loginFormGroup.submit, 62 | value: !(ctrlLoginFormGroupSubmitEnabledStreamValue), 63 | builder: (BuildContext context, event) { 64 | return Padding( 65 | padding: const EdgeInsets.all(0), 66 | child: ProgressButton( 67 | buttonState: ctrlStatusStreamValue, 68 | onPressed: event, 69 | child: Text( 70 | 'Login', 71 | ), 72 | ), 73 | ); 74 | }, 75 | ); 76 | }, 77 | ); 78 | }, 79 | ) 80 | `; 81 | 82 | const generated = generateWidget(xml); 83 | assertEqual(generated, expected); 84 | }); 85 | 86 | test(":formControl with :width wrapper", function() { 87 | const xml = ` 88 | <TextField :formControl="'quantity'" :width="80" /> 89 | `; 90 | 91 | const expected = ` 92 | StreamBuilder( 93 | initialData: ctrl.formGroup.get('quantity').value, 94 | stream: ctrl.formGroup.get('quantity').valueStream, 95 | builder: (BuildContext context, ctrlFormGroupGetQuantityValueStreamSnapshot) { 96 | return SizedBox( 97 | width: 80, 98 | child: TextField( 99 | controller: ctrl._attachController(ctrl.formGroup, 'quantity', () => TextEditingController()), 100 | ), 101 | ); 102 | }, 103 | ) 104 | `; 105 | 106 | const generated = generateWidget(xml); 107 | assertEqual(generated, expected); 108 | }); 109 | }); -------------------------------------------------------------------------------- /src/property-handlers/if.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("If custom property", function () { 4 | 5 | test("will generate if", function() { 6 | const xml = `<Text :if="true" />`; 7 | 8 | const expected = ` 9 | WidgetHelpers.ifTrue(true, 10 | () => Text( 11 | ), 12 | () => Container(width: 0, height: 0) 13 | )`; 14 | 15 | const generated = generateWidget(xml); 16 | assertEqual(generated, expected); 17 | }); 18 | 19 | 20 | test("will generate if wrapping :disable", function() { 21 | const xml = `<RasiedButton :if="true" :disable="!enabled" />`; 22 | 23 | const expected = ` 24 | WidgetHelpers.ifTrue(true, 25 | () => Disable( 26 | event: eventFunction, 27 | value: !enabled, 28 | builder: (BuildContext context, event) { 29 | return RasiedButton( 30 | ); 31 | }, 32 | ), 33 | () => Container(width: 0, height: 0) 34 | ) 35 | `; 36 | 37 | const generated = generateWidget(xml); 38 | assertEqual(generated, expected); 39 | }); 40 | 41 | 42 | test("will generate if around a wrapper property like padding", function() { 43 | const xml = `<Container :if="true" :padding="4"> 44 | <Text text="'hello'" /> 45 | </Container>`; 46 | 47 | const expected = ` 48 | WidgetHelpers.ifTrue(true, 49 | () => Container( 50 | child: Padding( 51 | padding: const EdgeInsets.all(4), 52 | child: Text( 53 | 'hello', 54 | ), 55 | ), 56 | ), 57 | () => Container(width: 0, height: 0) 58 | ) 59 | `; 60 | 61 | const generated = generateWidget(xml); 62 | assertEqual(generated, expected); 63 | }); 64 | }); -------------------------------------------------------------------------------- /src/property-handlers/if.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, ExtraDataModel, AttributeModel, PropertyModel } from '../models/models'; 4 | import { makeTabs } from "../utils"; 5 | import { PropertyResolver } from "../resolvers/property-resolver"; 6 | 7 | export class IfHandler extends CustomPropertyHandler { 8 | priority = 100000; 9 | valueSnippet = '${0:condition}'; 10 | 11 | constructor(private readonly propertyResolver: PropertyResolver) { 12 | super(); 13 | } 14 | 15 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 16 | return true; 17 | } 18 | 19 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 20 | let wrapperWidget: WidgetModel | null = null; 21 | let extraData: ExtraDataModel | null = null; 22 | const tempData: any = { falseWidgetId: '' }; 23 | const ifWidget: WidgetModel = { 24 | controllers: [], 25 | vars: [], 26 | formControls: [], 27 | properties: [], 28 | type: ':if', 29 | tempData: tempData, 30 | wrappedWidgets: [widget], 31 | onResolved: [], 32 | isCustom: true 33 | }; 34 | 35 | const result = this.propertyResolver.pipeValueResolver.resolve(element, attr.name, attr.value, ifWidget, true); 36 | wrapperWidget = result.wrapperWidget || ifWidget; 37 | tempData.condition = result.value; 38 | 39 | return { extraData, wrapperWidget, value: attr.value, handled: true }; 40 | } 41 | 42 | canGenerate(widget: WidgetModel): boolean { 43 | return true; 44 | } 45 | 46 | generate(widget: WidgetModel, tabsLevel: number, 47 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 48 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 49 | let code = ''; 50 | const tabs = makeTabs(tabsLevel); 51 | const data = widget.tempData; 52 | const wrappedWidget = widget.wrappedWidgets[0]; 53 | const elseWidget = widget.wrappedWidgets[1]; 54 | const defaultElseWidget = 'Container(width: 0, height: 0)'; // this must not be null (e.g. child of Row, StreamBuilder result can't be null) 55 | 56 | if (data && wrappedWidget) { 57 | code = `WidgetHelpers.ifTrue(${data.condition}, 58 | ${tabs} () => ${generateChildWidgetCode(wrappedWidget, tabsLevel + 1)}, 59 | ${tabs} () => ${elseWidget ? generateChildWidgetCode(elseWidget, tabsLevel + 1) : defaultElseWidget} 60 | ${tabs})`; 61 | } 62 | 63 | return code; 64 | } 65 | } -------------------------------------------------------------------------------- /src/property-handlers/item-builder-property.ts: -------------------------------------------------------------------------------- 1 | import { AttributeModel, WidgetModel } from "../models/models"; 2 | import { ItemBuilderHandler } from "./item-builder"; 3 | 4 | export class ItemBuilderPropertyHandler extends ItemBuilderHandler { 5 | isElement = false; 6 | valueSnippet = 'item of ${0:ctrl.items}'; 7 | 8 | protected resolveValueProperty(widget: WidgetModel, attr: AttributeModel): { grandparentWidget: WidgetModel | null, hasIndex: boolean | null, indexName: string } { 9 | let grandparentWidget = null; 10 | let indexName = 'index'; 11 | let hasIndex = false; 12 | let value = attr.value; 13 | 14 | // only resolve value if it is a property (value="item of items") and not a propertyElement 15 | if (typeof attr.value !== 'string') { 16 | return { grandparentWidget, hasIndex, indexName }; 17 | } 18 | 19 | // unwrap widget if it was custom 20 | if (widget.isCustom) { 21 | // todo search all nested not only the first child 22 | grandparentWidget = widget; 23 | widget = widget.wrappedWidgets[0]; 24 | } 25 | 26 | // if the property isn't an property element like <itemBuilder> 27 | const propertyElementProperties = widget.properties.filter(a => a.dataType !== 'widgetList' && a.dataType !== 'widget'); 28 | const widgetProp = widget.properties.filter(a => a.dataType === 'widgetList' || a.dataType === 'widget')[0]; 29 | const childWidget = (widgetProp ? widgetProp.value as WidgetModel : null) as any; 30 | 31 | if (widgetProp) { 32 | widget.properties.splice(widget.properties.indexOf(widgetProp), 1); 33 | } 34 | 35 | // add index if not present 36 | const ofIndex = value.indexOf(' of '); 37 | const indexIndex = value.indexOf(','); 38 | hasIndex = indexIndex < ofIndex && indexIndex !== -1; 39 | 40 | if (!hasIndex) { 41 | value = 'index, ' + value; 42 | } 43 | else { 44 | indexName = value.substring(0, indexIndex).trim(); 45 | } 46 | 47 | (attr.value as any) = { 48 | dataType: childWidget instanceof Array ? 'widgetList' : 'widget', 49 | // name: childResult.propertyElement, 50 | value: childWidget, 51 | extraData: { 52 | properties: [ 53 | ...propertyElementProperties, 54 | { 55 | dataType: 'object', value: value, name: 'data' 56 | } 57 | ] 58 | } 59 | }; 60 | 61 | return { grandparentWidget, hasIndex, indexName }; 62 | } 63 | } -------------------------------------------------------------------------------- /src/property-handlers/item-builder.ts: -------------------------------------------------------------------------------- 1 | import { BuilderHandler } from "./builder"; 2 | import { AttributeModel, WidgetModel, PropertyModel, AttributeInfo } from "../models/models"; 3 | import { PropertyResolveResult } from "../providers/property-handler-provider"; 4 | import * as parseXml from '../parser/types'; 5 | 6 | export class ItemBuilderHandler extends BuilderHandler { 7 | isElement = true; 8 | elementAttributes: AttributeInfo[] = [ 9 | { name: 'data', snippet: 'item of ${0:ctrl.items}' }, 10 | { name: 'params' } 11 | ]; 12 | valueSnippet = 'data="${0:item of ${1:ctrl.items}}" params="${3:context}"'; 13 | 14 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 15 | 16 | let { grandparentWidget, hasIndex, indexName } = this.resolveValueProperty(widget, attr); 17 | 18 | // console.log(attr); 19 | const data = attr.value as any; 20 | const properties = data.extraData.properties as PropertyModel[]; 21 | 22 | const nameIndex = properties.findIndex(a => a.name === 'name'); 23 | if (nameIndex > -1) { 24 | properties.splice(nameIndex, 1); 25 | } 26 | 27 | properties.push({ 28 | name: 'name', 29 | value: 'itemBuilder', 30 | dataType: 'object' 31 | }); 32 | // const nameIndex = properties.findIndex(a => a.name === 'name'); 33 | // if (nameIndex === -1) { 34 | // properties.push({ 35 | // name: 'name', 36 | // value: 'itemBuilder', 37 | // dataType: 'object' 38 | // }); 39 | // } 40 | 41 | const hasParams = properties.filter(a => a.name === 'params').length === 1; 42 | if (!hasParams) { 43 | properties.push({ 44 | name: 'params', 45 | value: 'BuildContext context, int ' + indexName + (widget.type === 'AnimatedList' ? ', Animation animation' : ''), 46 | dataType: 'object' 47 | }); 48 | 49 | const dataProp = properties.filter(a => a.name === 'data')[0]; 50 | if (dataProp) { 51 | // add index variable name to data if not present 52 | const ofIndex = (dataProp.value as string).indexOf(' of '); 53 | const indexIndex = (dataProp.value as string).indexOf(','); 54 | hasIndex = indexIndex < ofIndex && indexIndex !== -1; 55 | if (!hasIndex) { 56 | dataProp.value = indexName + ', ' + dataProp.value; 57 | } 58 | } 59 | } 60 | 61 | const resulveResult = super.resolve(element, attr, widget); 62 | 63 | // re-wrap widget if it was custom 64 | if (grandparentWidget) { 65 | grandparentWidget.wrappedWidgets = [resulveResult.wrapperWidget as any]; 66 | resulveResult.wrapperWidget = grandparentWidget; 67 | } 68 | 69 | return resulveResult; 70 | } 71 | 72 | protected resolveValueProperty(widget: WidgetModel, attr: AttributeModel): { grandparentWidget: WidgetModel | null, hasIndex: boolean | null, indexName: string } { 73 | let grandparentWidget = null; 74 | let indexName = 'index'; 75 | let hasIndex = false; 76 | let value = attr.value; 77 | 78 | return { grandparentWidget, hasIndex, indexName }; 79 | } 80 | } -------------------------------------------------------------------------------- /src/property-handlers/repeat.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Repeat Tests", function () { 4 | 5 | test("basic", function() { 6 | const xml = `<Text :repeat="item of items" />`; 7 | 8 | const expected = ` 9 | ...WidgetHelpers.mapToWidgetList(items, (item, index) { 10 | return Text( 11 | ); 12 | } 13 | )`; 14 | 15 | const generated = generateWidget(xml); 16 | assertEqual(generated, expected); 17 | }); 18 | 19 | test("with wrapper :margin", function() { 20 | const xml = `<Text :repeat="item of items" :margin="10" />`; 21 | 22 | const expected = ` 23 | ...WidgetHelpers.mapToWidgetList(items, (item, index) { 24 | return Padding( 25 | padding: const EdgeInsets.all(10), 26 | child: Text( 27 | 28 | ), 29 | ); 30 | } 31 | ) 32 | `; 33 | 34 | const generated = generateWidget(xml); 35 | assertEqual(generated, expected); 36 | }); 37 | 38 | test("with (if)", function() { 39 | const xml = `<Text :repeat="item of items" :if="condition" />`; 40 | 41 | const expected = ` 42 | ...WidgetHelpers.mapToWidgetList(items, (item, index) { 43 | return WidgetHelpers.ifTrue(condition, 44 | () => Text( 45 | 46 | ), 47 | () => Container(width: 0, height: 0) 48 | ); 49 | } 50 | ) 51 | `; 52 | 53 | const generated = generateWidget(xml); 54 | assertEqual(generated, expected); 55 | }); 56 | 57 | test("inside column", function() { 58 | const xml = ` 59 | <Column> 60 | <Text :repeat="item of items" /> 61 | </Column> 62 | `; 63 | 64 | const expected = ` 65 | Column( 66 | children: [ 67 | ...WidgetHelpers.mapToWidgetList(items, (item, index) { 68 | return Text( 69 | 70 | ); 71 | }), 72 | ], 73 | ) 74 | `; 75 | 76 | const generated = generateWidget(xml); 77 | assertEqual(generated, expected); 78 | }); 79 | 80 | test("multiple inside column", function() { 81 | const xml = ` 82 | <Column> 83 | <Text :repeat="int item of items" /> 84 | <Text :repeat="item of items" /> 85 | <Text :repeat="item of items" /> 86 | </Column> 87 | `; 88 | 89 | const expected = ` 90 | Column( 91 | children: [ 92 | ...WidgetHelpers.mapToWidgetList(items, (int item, index) { 93 | return Text( 94 | 95 | ); 96 | } 97 | ), 98 | ...WidgetHelpers.mapToWidgetList(items, (item, index) { 99 | return Text( 100 | 101 | ); 102 | } 103 | ), 104 | ...WidgetHelpers.mapToWidgetList(items, (item, index) { 105 | return Text( 106 | 107 | ); 108 | } 109 | ), 110 | ], 111 | ) 112 | `; 113 | 114 | const generated = generateWidget(xml); 115 | assertEqual(generated, expected); 116 | }); 117 | 118 | test("inside builder that returns list of widgets e.g. PopupMenuButton", function() { 119 | const xml = ` 120 | <PopupMenuButton> 121 | <builder name="itemBuilder"> 122 | <PopupMenuItem :repeat="MenuItem menuItem of component.menuItems" value="menuItem"> 123 | <Text text="menuItem.title" /> 124 | </PopupMenuItem> 125 | </builder> 126 | </PopupMenuButton> 127 | `; 128 | 129 | const expected = ` 130 | PopupMenuButton( 131 | itemBuilder: (BuildContext context) { 132 | return WidgetHelpers.mapToWidgetList(component.menuItems, (MenuItem menuItem, index) { 133 | return PopupMenuItem( 134 | value: menuItem, 135 | child: Text( 136 | menuItem.title, 137 | ), 138 | ); 139 | } 140 | ); 141 | }, 142 | ) 143 | `; 144 | 145 | const generated = generateWidget(xml); 146 | assertEqual(generated, expected); 147 | }); 148 | }); -------------------------------------------------------------------------------- /src/property-handlers/repeat.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, ExtraDataModel, AttributeModel, PropertyModel } from '../models/models'; 4 | import { extractForLoopParams, makeTabs, spaceAfter } from "../utils"; 5 | import { PropertyResolver } from "../resolvers/property-resolver"; 6 | 7 | export class RepeatHandler extends CustomPropertyHandler { 8 | priority = 1000000; // top priority 9 | valueSnippet = 'item of ${0:items}'; 10 | 11 | constructor(private readonly propertyResolver: PropertyResolver) { 12 | super(); 13 | } 14 | 15 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 16 | return true; 17 | } 18 | 19 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 20 | let wrapperWidget: WidgetModel | null = null; 21 | let extraData: ExtraDataModel | null = null; 22 | 23 | const { listName, indexName, itemName, typeName } = extractForLoopParams(attr.value); 24 | const tempData: any = { listName, indexName, itemName, typeName, widget }; 25 | const listNameWithPipes = attr.value.substr(attr.value.indexOf(listName)); 26 | const contentWidget: WidgetModel = { 27 | controllers: [], 28 | vars: [], 29 | formControls: [], 30 | properties: [ 31 | { 32 | dataType: 'function', 33 | name: ':repeat', 34 | value: '' 35 | }, 36 | { 37 | dataType: 'widget', 38 | name: 'child', 39 | value: widget, 40 | skipGeneratingCode: true 41 | } 42 | ], 43 | type: '', // empty type to generate properties only and skip generating widget constractor e.g.: Container(...) 44 | tempData: tempData, 45 | wrappedWidgets: [], // don't add the widget 46 | onResolved: [] 47 | }; 48 | 49 | // the user must not use 'stream' or 'future' pipes wth repeat 50 | const result = this.propertyResolver.pipeValueResolver.resolve(element, attr.name, listNameWithPipes, contentWidget); 51 | wrapperWidget = result.wrapperWidget || contentWidget; 52 | tempData.listValueVariableName = result.wrapperWidget ? result.value : tempData.listName; 53 | 54 | return { extraData, wrapperWidget, value: result.value, handled: true }; 55 | } 56 | 57 | canGenerate(widget: WidgetModel): boolean { 58 | return true; 59 | } 60 | 61 | generate(widget: WidgetModel, tabsLevel: number, 62 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 63 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 64 | const tabs = makeTabs(tabsLevel - 1); 65 | const data = widget.tempData; 66 | const originalWidget = data.widget as WidgetModel; 67 | 68 | const code = 69 | `...WidgetHelpers.mapToWidgetList(${data.listValueVariableName}, (${spaceAfter(data.typeName)}${data.itemName || 'item'}, ${data.indexName || 'index'}) { 70 | ${tabs} return ${generateChildWidgetCode(originalWidget, tabsLevel + 1)}; 71 | ${tabs} } 72 | ${tabs})`; 73 | 74 | return code; 75 | } 76 | } -------------------------------------------------------------------------------- /src/property-handlers/switch-case.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, AttributeModel, PropertyModel } from '../models/models'; 4 | 5 | export class SwitchCaseHandler extends CustomPropertyHandler { 6 | 7 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 8 | return true; 9 | } 10 | 11 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 12 | return { 13 | extraData: null, 14 | wrapperWidget: widget, 15 | value: attr.value, 16 | handled: false // keep it as property to be used in SwitchResolver 17 | }; 18 | } 19 | 20 | canGenerate(widget: WidgetModel): boolean { 21 | return true; 22 | } 23 | 24 | generate(widget: WidgetModel, tabsLevel: number, 25 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 26 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 27 | return ''; // don't generate code for this property as it is custom 28 | } 29 | } -------------------------------------------------------------------------------- /src/property-handlers/switch.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, ExtraDataModel, AttributeModel, PropertyModel } from '../models/models'; 4 | import { makeTabs } from "../utils"; 5 | import { PropertyResolver } from "../resolvers/property-resolver"; 6 | 7 | export class SwitchHandler extends CustomPropertyHandler { 8 | priority = -100000; // lowest priority 9 | 10 | constructor(private readonly propertyResolver: PropertyResolver) { 11 | super(); 12 | } 13 | 14 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 15 | return true; 16 | } 17 | 18 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 19 | let wrapperWidget: WidgetModel | null = null; 20 | let extraData: ExtraDataModel | null = null; 21 | 22 | const { casesParentWidget, casesProperty } = this.getSwitchCasesAndParent(widget); 23 | const tempData: any = { 24 | casesWidgets: JSON.parse(JSON.stringify(casesProperty.value)) // copy them 25 | }; 26 | 27 | const switchWidget: WidgetModel = { 28 | controllers: [], 29 | vars: [], 30 | formControls: [], 31 | properties: [], 32 | type: ':switch', 33 | tempData: tempData, 34 | wrappedWidgets: [], //[widget] 35 | onResolved: [] 36 | }; 37 | 38 | // remove cases from original widget to replace the new one 39 | const casesWidgets = casesProperty.value as WidgetModel[]; 40 | for (let index = 0; index < casesWidgets.length; index++) { 41 | const a = casesWidgets[index]; 42 | const hasSwitchCase = (a.properties as any[]).filter(p => p.name === ':switchCase').length === 1; 43 | if (hasSwitchCase) { 44 | casesWidgets.splice(casesWidgets.findIndex(t => a === t), 1); 45 | index--; 46 | } 47 | } 48 | 49 | // put switch widget inside the widget that has child or children contains cases 50 | casesProperty.value = switchWidget; 51 | casesProperty.name = 'child'; 52 | casesProperty.dataType = 'widget'; 53 | 54 | const result = this.propertyResolver.pipeValueResolver.resolve(element, attr.name, attr.value, casesParentWidget, true); 55 | wrapperWidget = result.wrapperWidget || casesParentWidget; 56 | 57 | // update switch value after apply binding 58 | tempData.switchValue = result.value; 59 | 60 | return { extraData, wrapperWidget, value: attr.value, handled: true }; 61 | } 62 | 63 | private getSwitchCasesAndParent(widget: WidgetModel): { casesProperty: PropertyModel, casesParentWidget: WidgetModel } { 64 | const childrenWidgets = widget.properties.filter(a => a.dataType === 'widgetList'); 65 | const childWidget = widget.properties.filter(a => a.dataType === 'widget'); 66 | if (childWidget.length) { 67 | childrenWidgets.push(childWidget[0]); 68 | } 69 | if (childrenWidgets.length) { 70 | return { casesProperty: childrenWidgets[0], casesParentWidget: widget }; 71 | } 72 | 73 | if (widget.wrappedWidgets) { 74 | for (const w of widget.wrappedWidgets) { 75 | const result = this.getSwitchCasesAndParent(w); 76 | if (result.casesParentWidget) { 77 | if (result.casesProperty) { 78 | return result; 79 | } 80 | return this.getSwitchCasesAndParent(w); 81 | } 82 | } 83 | } 84 | 85 | return { casesProperty: null as any, casesParentWidget: null as any }; 86 | } 87 | 88 | canGenerate(widget: WidgetModel): boolean { 89 | return true; 90 | } 91 | 92 | generate(widget: WidgetModel, tabsLevel: number, 93 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 94 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 95 | const tabs = makeTabs(tabsLevel); 96 | const tempData = widget.tempData; 97 | const casesWidgets = (tempData.casesWidgets instanceof Array ? tempData.casesWidgets : [tempData.casesWidgets]) as WidgetModel[]; 98 | const defaultWidget = 'Container(width: 0, height: 0)'; 99 | let cases = ''; 100 | let code = ''; 101 | 102 | if (casesWidgets) { 103 | casesWidgets.filter(a => !!a).forEach(w => { 104 | const caseProp = w.properties.filter(a => a.name === ':switchCase')[0]; 105 | if (caseProp) { 106 | cases += `\n${tabs} new SwitchCase(${caseProp.value}, \n${tabs} () => ${generateChildWidgetCode(w, tabsLevel + 3)}\n${tabs} ),`; 107 | } 108 | }); 109 | } 110 | 111 | code += `WidgetHelpers.switchValue(\n${tabs} ${tempData.switchValue},\n${tabs} () => ${defaultWidget},\n${tabs} [${cases}\n${tabs} ]\n${tabs})`; 112 | return code; 113 | } 114 | } -------------------------------------------------------------------------------- /src/property-handlers/wrapper-animation.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Wrapper Animation Property Tests", function () { 4 | 5 | test("test 1", function() { 6 | const xml = ` 7 | <Transform :use="translate"> 8 | <apply-animation curve="easeOut" duration="milliseconds: 300" autoTrigger> 9 | <offset type="Offset" begin="Offset(-10, 0)" end="Offset(0, 0)" /> 10 | </apply-animation> 11 | <Container color="red" width="200" height="200" /> 12 | </Transform> 13 | `; 14 | 15 | const expected = ` 16 | AnimationBuilder( 17 | autoTrigger: true, 18 | curve: Curves.easeOut, 19 | duration: Duration(milliseconds: 300), 20 | tweenMap: { 21 | "offset": Tween<Offset>(begin: Offset(-10, 0), end: Offset(0, 0)) 22 | }, 23 | builderMap: (Map<String, Animation> animations, Widget child) { 24 | return Transform.translate( 25 | offset: animations["offset"].value, 26 | child: Container( 27 | color: Colors.red, 28 | height: 200, 29 | width: 200, 30 | ), 31 | ); 32 | }, 33 | ) 34 | `; 35 | 36 | const generated = generateWidget(xml); 37 | assertEqual(generated, expected); 38 | }); 39 | 40 | test("test 2", function() { 41 | const xml = ` 42 | <Container> 43 | <apply-animation duration="milliseconds: 1000" autoTrigger cycles="5"> 44 | <color type="color" begin="Colors.transparent" end="Colors.white" /> 45 | <width type="int" begin="100" end="200" /> 46 | <height type="int" begin="100" end="300" /> 47 | </apply-animation> 48 | </Container> 49 | `; 50 | 51 | const expected = ` 52 | AnimationBuilder( 53 | autoTrigger: true, 54 | cycles: 5, 55 | duration: Duration(milliseconds: 1000), 56 | tweenMap: { 57 | "height": Tween<int>(begin: 100, end: 300), 58 | "width": Tween<int>(begin: 100, end: 200), 59 | "color": ColorTween(begin: Colors.transparent, end: Colors.white) 60 | }, 61 | builderMap: (Map<String, Animation> animations, Widget child) { 62 | return Container( 63 | color: animations["color"].value, 64 | height: animations["height"].value, 65 | width: animations["width"].value, 66 | ); 67 | }, 68 | ) 69 | `; 70 | 71 | const generated = generateWidget(xml); 72 | assertEqual(generated, expected); 73 | }); 74 | 75 | test("test 3", function() { 76 | const xml = ` 77 | <Container> 78 | <apply-animation name="myAnimation" duration="seconds: 1" > 79 | <color type="color" begin="Colors.blue" end="Colors.red" /> 80 | <width type="double" begin="100" end="200" /> 81 | <height type="double" begin="100" end="300" /> 82 | </apply-animation> 83 | </Container> 84 | `; 85 | 86 | const expected = ` 87 | AnimationBuilder( 88 | duration: Duration(seconds: 1), 89 | key: ctrl._myAnimationKey, 90 | tweenMap: { 91 | "height": Tween<double>(begin: 100, end: 300), 92 | "width": Tween<double>(begin: 100, end: 200), 93 | "color": ColorTween(begin: Colors.blue, end: Colors.red) 94 | }, 95 | builderMap: (Map<String, Animation> animations, Widget child) { 96 | return Container( 97 | color: animations["color"].value, 98 | height: animations["height"].value, 99 | width: animations["width"].value, 100 | ); 101 | }, 102 | ) 103 | `; 104 | 105 | const generated = generateWidget(xml); 106 | assertEqual(generated, expected); 107 | }); 108 | }); -------------------------------------------------------------------------------- /src/property-handlers/wrapper-animation.ts: -------------------------------------------------------------------------------- 1 | import { WidgetModel, PropertyModel, VariableModel, AttributeInfo } from '../models/models'; 2 | import { WrapperPropertyHandler } from "./wrapper-property"; 3 | import { makeTabs } from '../utils'; 4 | import { PropertyResolver } from '../resolvers/property-resolver'; 5 | 6 | export class WrapperAnimationHandler extends WrapperPropertyHandler { 7 | isElement = true; 8 | elementAttributes: AttributeInfo[] = [ 9 | { name: 'name' }, 10 | { name: 'duration', snippet: 'millisecond: ${0:250}' }, 11 | { name: 'cycles' }, 12 | { name: 'repeats' }, 13 | { name: 'autoTrigger' }, 14 | { name: 'curve' } 15 | ]; 16 | 17 | constructor(propertyResolver: PropertyResolver) { 18 | super(propertyResolver, [{ handler: 'apply-animation', targetProperty: 'animation' }], 'AnimationBuilder'); 19 | } 20 | 21 | protected createWrapperWidget(widget: WidgetModel, targetProperty: string, value: string, onWrapped: ((wrapper: WidgetModel) => void)[]): { wrapperWidget: WidgetModel, propertyToUpdateAfterBinding: PropertyModel | null } { 22 | if (typeof value === 'string') { 23 | return null as any; 24 | } 25 | 26 | let properties: PropertyModel[] = (value as any).extraData.properties; 27 | const tweenResult = this.getTweens(properties, widget); 28 | const vars: VariableModel[] = []; 29 | 30 | const nameProp = properties.filter(a => a.name === 'name')[0]; 31 | if (nameProp) { 32 | const animationControllerName = nameProp.value; 33 | properties.push({ 34 | name: 'key', 35 | value: `ctrl._${animationControllerName}Key`, 36 | dataType: 'object' 37 | }); 38 | vars.push({ 39 | name:`_${animationControllerName}Key`, 40 | type: 'GlobalKey<AnimationBuilderState>', 41 | value:`GlobalKey<AnimationBuilderState>()` 42 | }); 43 | vars.push({ 44 | name:`AnimationBuilderStateMixin get ${animationControllerName} => _${animationControllerName}Key.currentState;`, 45 | type: '', 46 | value:`` 47 | }); 48 | } 49 | 50 | properties = properties.filter(a => a.dataType === 'object' && a.name !== 'name'); 51 | properties.forEach(p => { 52 | switch (p.name) { 53 | case 'duration': 54 | p.value = p.value.toString().indexOf(':') > -1 && p.value.toString().indexOf('(') === -1 ? `Duration(${p.value})` : p.value; 55 | break; 56 | case 'autoTrigger': 57 | p.value = p.value === 'true' || p.value !== 'false' ? 'true' : 'false'; 58 | break; 59 | case 'curve': 60 | p.value = (p.value as string).indexOf('.') > -1 ? p.value : `Curves.${p.value}`; 61 | break; 62 | } 63 | }); 64 | 65 | properties.push({ 66 | dataType: 'function', 67 | name: 'builderMap', 68 | value: '', 69 | extraData: { 70 | parameters: [ 71 | { name: 'animations', type: 'Map<String, Animation>' }, 72 | { name: `child`, type: 'Widget' } 73 | ], 74 | addReturn: true 75 | } 76 | }); 77 | 78 | tweenResult.values.forEach(v => { 79 | const prop = widget.properties.filter(a => a.name === v.propertyName)[0]; 80 | if (prop) { 81 | prop.value = v.propertyValue; 82 | } 83 | else { 84 | widget.properties.push({ dataType: 'object', name: v.propertyName, value: v.propertyValue }); 85 | } 86 | }); 87 | 88 | const wrapperWidget: WidgetModel = { 89 | controllers: [], 90 | vars: vars, 91 | formControls: [], 92 | properties: [ 93 | tweenResult.tweenMap, 94 | ...properties 95 | ], 96 | type: this.targetWidgetType, 97 | wrappedWidgets: [widget], 98 | onResolved: [], 99 | mixins: ['TickerProviderStateMixin'] 100 | }; 101 | return { wrapperWidget, propertyToUpdateAfterBinding: null }; 102 | } 103 | 104 | private getTweens(properties: PropertyModel[], widget: WidgetModel): { tweenMap: PropertyModel, values: { propertyName: string, propertyValue: string}[] } { 105 | const tweens = properties 106 | .filter(a => a.dataType === 'propertyElement') 107 | .map(a => { 108 | let props = []; 109 | if ((a.value as any).extraData) { 110 | props = ((a.value as any).extraData.properties as any[]); 111 | } 112 | return this.createTween(a.name, props); 113 | }) 114 | .filter(Boolean); 115 | 116 | const isTransitionWidget = widget.type.endsWith('Transition'); 117 | const tweenMap: PropertyModel = { 118 | dataType: 'object', 119 | value: tweens, 120 | name: 'tweenMap', 121 | generateCodeDelegate: this.generateTweensCodeDelegate 122 | }; 123 | 124 | const values = tweens.map(t => { 125 | return { 126 | propertyName: t.property, 127 | propertyValue: `animations["${t.property}"]${isTransitionWidget ? '' : '.value'}` 128 | }; 129 | }); 130 | 131 | return { 132 | tweenMap, values 133 | }; 134 | } 135 | 136 | private createTween(name: string, properties: PropertyModel[]): any { 137 | const begin = properties.filter(a => a.name === 'begin')[0]; 138 | const end = properties.filter(a => a.name === 'end')[0]; 139 | const type = properties.filter(a => a.name === 'type')[0]; 140 | 141 | if (!type) { 142 | return null; 143 | } 144 | 145 | return { 146 | property: name, 147 | begin: begin.value, 148 | end: end.value, 149 | type: this.getTweenType(type.value as string) 150 | }; 151 | } 152 | 153 | private getTweenType(type: string) { 154 | let result = type; 155 | const knownTypes = ['int', 'double', 'offset']; 156 | let normalizedType = type[0].toUpperCase() + (type.length > 1 ? type.substring(1) : ''); 157 | 158 | if (knownTypes.filter(a => a === type.toLowerCase()).length > 0) { 159 | if (type === 'offset') { 160 | result = `Tween<${normalizedType}>`; 161 | } 162 | else { 163 | result = `Tween<${type}>`; 164 | } 165 | } 166 | else { 167 | result = `${normalizedType}Tween`; 168 | } 169 | 170 | return result; 171 | } 172 | 173 | private generateTweensCodeDelegate(widget: WidgetModel, property: PropertyModel, tabsLevel: number): string { 174 | const tweens = property.value as any[]; 175 | const tabs = makeTabs(tabsLevel); 176 | const code = tweens 177 | .map(a => { 178 | return `${tabs} "${a.property}": ${a.type}(begin: ${a.begin}, end: ${a.end})`; 179 | }) 180 | .join(',\n'); 181 | return `${tabs}tweenMap: {\n${code}\n${tabs}}`; 182 | } 183 | } -------------------------------------------------------------------------------- /src/property-handlers/wrapper-consumer-property.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Wrapper Consumer Property Tests", function () { 4 | 5 | test("basic", function() { 6 | const xml = ` 7 | <Column :consumer="MyProvider myProvider"> 8 | <Text text="myProvider.myVariable" /> 9 | </Column> 10 | `; 11 | 12 | const expected = ` 13 | Consumer<MyProvider>( 14 | builder: (BuildContext context, MyProvider myProvider, Widget child) { 15 | return Column( 16 | children: [ 17 | Text( 18 | myProvider.myVariable, 19 | ), 20 | ], 21 | ); 22 | }, 23 | ) 24 | `; 25 | 26 | const generated = generateWidget(xml); 27 | assertEqual(generated, expected); 28 | }); 29 | 30 | test("with another wrapper property like (margin)", function() { 31 | const xml = ` 32 | <Column :consumer="MyProvider myProvider" :margin="5"> 33 | <Text text="myProvider.myVariable" /> 34 | </Column> 35 | `; 36 | 37 | const expected = ` 38 | Consumer<MyProvider>( 39 | builder: (BuildContext context, MyProvider myProvider, Widget child) { 40 | return Padding( 41 | padding: const EdgeInsets.all(5), 42 | child: Column( 43 | children: [ 44 | Text( 45 | myProvider.myVariable, 46 | ), 47 | ], 48 | ), 49 | ); 50 | }, 51 | ) 52 | `; 53 | 54 | const generated = generateWidget(xml); 55 | assertEqual(generated, expected); 56 | }); 57 | }); -------------------------------------------------------------------------------- /src/property-handlers/wrapper-consumer-property.ts: -------------------------------------------------------------------------------- 1 | import { WidgetModel, PropertyModel } from '../models/models'; 2 | import { WrapperPropertyHandler } from "./wrapper-property"; 3 | import { PropertyResolver } from '../resolvers/property-resolver'; 4 | 5 | export class WrapperConsumerPropertyHandler extends WrapperPropertyHandler { 6 | valueSnippet = '${0:DataType} ${1:instanceName}'; 7 | 8 | constructor(propertyResolver: PropertyResolver) { 9 | super(propertyResolver,[{ handler: ':consumer', targetProperty: 'consumer' }], 'Consumer', undefined, 10000); // top priority but less than if & repeat 10 | } 11 | 12 | protected createWrapperWidget(widget: WidgetModel, targetProperty: string, value: string, onWrapped: ((wrapper: WidgetModel) => void)[]): { wrapperWidget: WidgetModel, propertyToUpdateAfterBinding: PropertyModel | null } { 13 | const parts = value.split(' '); 14 | const providerTypeName = parts[0]; 15 | const providerInstanceName = parts[1]; 16 | 17 | const wrapperWidget: WidgetModel = { 18 | controllers: [], 19 | vars: [], 20 | formControls: [], 21 | properties: [ 22 | { 23 | dataType: 'function', 24 | name: 'builder', 25 | value: '', 26 | extraData: { 27 | parameters: [ 28 | { name: 'context', type: 'BuildContext' }, 29 | { name: providerInstanceName, type: providerTypeName }, 30 | { name: 'child', type: 'Widget' } 31 | ], 32 | addReturn: true 33 | } 34 | } 35 | ], 36 | type: `${this.targetWidgetType}<${providerTypeName}>`, 37 | wrappedWidgets: [widget], 38 | onResolved: [] 39 | }; 40 | return { wrapperWidget, propertyToUpdateAfterBinding: null }; 41 | } 42 | } -------------------------------------------------------------------------------- /src/property-handlers/wrapper-disable-property.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Wrapper Disable Property Tests", function () { 4 | 5 | test("basic", function() { 6 | const xml = ` 7 | <RaisedButton onPressed="component.login" :disable="component.formGroup.status"> 8 | <Text text="'Login'" /> 9 | </RaisedButton> 10 | `; 11 | 12 | const expected = ` 13 | Disable( 14 | event: component.login, 15 | value: component.formGroup.status, 16 | builder: (BuildContext context, event) { 17 | 18 | return RaisedButton( 19 | onPressed: event, 20 | child: Text( 21 | 'Login', 22 | ), 23 | ); 24 | }, 25 | ) 26 | `; 27 | 28 | const generated = generateWidget(xml); 29 | assertEqual(generated, expected); 30 | }); 31 | 32 | test("with stream", function() { 33 | const xml = ` 34 | <RaisedButton onPressed="component.login" :disable="component.formGroup.statusStream | stream"> 35 | <Text text="'Login'" /> 36 | </RaisedButton> 37 | `; 38 | 39 | const expected = ` 40 | StreamBuilder( 41 | initialData: null, 42 | stream: component.formGroup.statusStream, 43 | builder: (BuildContext context, componentFormGroupStatusStreamSnapshot) { 44 | final componentFormGroupStatusStreamValue = componentFormGroupStatusStreamSnapshot.data; 45 | return Disable( 46 | event: component.login, 47 | value: componentFormGroupStatusStreamValue, 48 | builder: (BuildContext context, event) { 49 | return RaisedButton( 50 | onPressed: event, 51 | child: Text( 52 | 'Login', 53 | ), 54 | ); 55 | }, 56 | ); 57 | }, 58 | ) 59 | `; 60 | 61 | const generated = generateWidget(xml); 62 | assertEqual(generated, expected); 63 | }); 64 | 65 | test("with another wrapper property like (margin)", function() { 66 | const xml = ` 67 | <RaisedButton onPressed="component.login" :margin="4" :disable="component.formGroup.status"> 68 | <Text text="'Login'" /> 69 | </RaisedButton> 70 | `; 71 | 72 | const expected = ` 73 | Disable( 74 | event: component.login, 75 | value: component.formGroup.status, 76 | builder: (BuildContext context, event) { 77 | return Padding( 78 | padding: const EdgeInsets.all(4), 79 | child: RaisedButton( 80 | onPressed: event, 81 | child: Text( 82 | 'Login', 83 | ), 84 | ), 85 | ); 86 | }, 87 | ) 88 | `; 89 | 90 | const generated = generateWidget(xml); 91 | assertEqual(generated, expected); 92 | }); 93 | 94 | test("with ChildWrapperProperty like (padding)", function() { 95 | const xml = ` 96 | <RaisedButton onPressed="component.login" :disable="component.formGroup.status" :padding="4"> 97 | <Text text="'Login'" /> 98 | </RaisedButton> 99 | `; 100 | 101 | const expected = ` 102 | Disable( 103 | event: component.login, 104 | value: component.formGroup.status, 105 | builder: (BuildContext context, event) { 106 | 107 | return RaisedButton( 108 | onPressed: event, 109 | child: Padding( 110 | padding: const EdgeInsets.all(4), 111 | child: Text( 112 | 'Login', 113 | ), 114 | ), 115 | ); 116 | }, 117 | ) 118 | `; 119 | 120 | const generated = generateWidget(xml); 121 | assertEqual(generated, expected); 122 | }); 123 | }); -------------------------------------------------------------------------------- /src/property-handlers/wrapper-disable-property.ts: -------------------------------------------------------------------------------- 1 | import { WidgetModel, PropertyModel } from '../models/models'; 2 | import { WrapperPropertyHandler } from "./wrapper-property"; 3 | import { makeVariableName } from "../utils"; 4 | import { PropertyResolver } from '../resolvers/property-resolver'; 5 | 6 | export class WrapperDisablePropertyHandler extends WrapperPropertyHandler { 7 | constructor(propertyResolver: PropertyResolver) { 8 | super(propertyResolver,[{ handler: ':disable', targetProperty: 'value' }], 'Disable', undefined, 9000); // greater than other wrappers and less than consumer & stream 9 | } 10 | 11 | protected createWrapperWidget(widget: WidgetModel, targetProperty: string, value: string, onWrapped: ((wrapper: WidgetModel) => void)[]): { wrapperWidget: WidgetModel, propertyToUpdateAfterBinding: PropertyModel | null } { 12 | const wrapperWidget: WidgetModel = { 13 | controllers: [], 14 | vars: [], 15 | formControls: [], 16 | properties: [ 17 | { 18 | dataType: 'object', value: value, name: 'value' 19 | }, 20 | { 21 | dataType: 'object', value: 'eventFunction', name: 'event' 22 | }, 23 | { 24 | dataType: 'function', 25 | name: 'builder', 26 | value: '', 27 | extraData: { 28 | parameters: [ 29 | { name: 'context', type: 'BuildContext' }, 30 | { name: `event`, type: '' } 31 | ], 32 | addReturn: true 33 | } 34 | } 35 | ], 36 | type: this.targetWidgetType, 37 | wrappedWidgets: [widget], 38 | onResolved: [] 39 | }; 40 | 41 | widget.onResolved.push((w) => { 42 | const firstEvent = this.getfirstEvent(w); 43 | if (firstEvent) { 44 | wrapperWidget.properties[1].value = firstEvent.value; 45 | firstEvent.value = 'event'; 46 | } 47 | }); 48 | 49 | onWrapped.push((wrapper) => { 50 | // remove (:if) statement from the body of builder is StreamBuilder 51 | // so Disable will be returned regardless of the result of snapshot 52 | if (wrapper.type === 'StreamBuilder') { 53 | (wrapper.properties[2].extraData as any).logic = [(wrapper.properties[2].extraData as any).logic[0]]; 54 | } 55 | }); 56 | 57 | return { wrapperWidget, propertyToUpdateAfterBinding: wrapperWidget.properties[0] }; 58 | } 59 | 60 | private getfirstEvent(w: WidgetModel): any { 61 | // const event = w.properties.filter(a => a.isEvent)[0]; 62 | const event = w.properties.filter(a => /^on[A-Z].*$/g.test(a.name))[0]; 63 | if (event) { 64 | return event; 65 | } 66 | if (w.wrappedWidgets[0]) { 67 | return this.getfirstEvent(w.wrappedWidgets[0]); 68 | } 69 | return null; 70 | } 71 | } -------------------------------------------------------------------------------- /src/property-handlers/wrapper-property.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Wrapper Properties Tests", function () { 4 | 5 | test("basic", function() { 6 | const xml = `<Text :opacity="0" />`; 7 | 8 | const expected = ` 9 | Opacity( 10 | opacity: 0, 11 | child: Text( 12 | 13 | ), 14 | )`; 15 | 16 | const generated = generateWidget(xml); 17 | assertEqual(generated, expected); 18 | }); 19 | 20 | test("with default value for property (topCenter)", function() { 21 | const xml = `<Text text="'test'" :topCenter />`; 22 | 23 | const expected = ` 24 | Align( 25 | alignment: Alignment.topCenter, 26 | child: Text( 27 | 'test', 28 | ), 29 | ) 30 | `; 31 | 32 | const generated = generateWidget(xml); 33 | assertEqual(generated, expected); 34 | }); 35 | 36 | test("with default value for property (topCenter) with user value (user value should ignored)", function() { 37 | const xml = `<Text text="'test'" :topCenter="some values" />`; 38 | 39 | const expected = ` 40 | Align( 41 | alignment: Alignment.topCenter, 42 | child: Text( 43 | 'test', 44 | ), 45 | ) 46 | `; 47 | 48 | const generated = generateWidget(xml); 49 | assertEqual(generated, expected); 50 | }); 51 | 52 | test("wrapper without property name", function() { 53 | const xml = `<TestWidget :testText="'test'" />`; 54 | 55 | const expected = ` 56 | Text( 57 | 'test', 58 | child: TestWidget(), 59 | ) 60 | `; 61 | 62 | const generated = generateWidget(xml); 63 | assertEqual(generated, expected); 64 | }); 65 | 66 | 67 | test("with stream", function() { 68 | const xml = `<Text :opacity="value | stream" />`; 69 | 70 | const expected = ` 71 | StreamBuilder( 72 | initialData: null, 73 | stream: value, 74 | builder: (BuildContext context, valueSnapshot) { 75 | final valueValue = valueSnapshot.data; 76 | if (valueValue == null) { 77 | return Container(width: 0, height: 0); 78 | } 79 | return Opacity( 80 | opacity: valueValue, 81 | child: Text( 82 | 83 | ), 84 | ); 85 | }, 86 | ) 87 | `; 88 | 89 | const generated = generateWidget(xml); 90 | assertEqual(generated, expected); 91 | }); 92 | 93 | 94 | test("with other pipe", function() { 95 | const xml = `<Text :opacity="value | somePipe" />`; 96 | 97 | const expected = ` 98 | Opacity( 99 | opacity: _pipeProvider.transform(context, "somePipe", value, []), 100 | child: Text( 101 | 102 | ), 103 | ) 104 | `; 105 | 106 | const generated = generateWidget(xml); 107 | assertEqual(generated, expected); 108 | }); 109 | 110 | 111 | test("Related properties :width", function() { 112 | const xml = ` 113 | <RaisedButton onPressed="event" :width="100"> 114 | <Text text="'Login'" /> 115 | </RaisedButton> 116 | `; 117 | 118 | const expected = ` 119 | SizedBox( 120 | width: 100, 121 | child: RaisedButton( 122 | onPressed: event, 123 | child: Text( 124 | 'Login', 125 | ), 126 | ), 127 | ) 128 | `; 129 | 130 | const generated = generateWidget(xml); 131 | assertEqual(generated, expected); 132 | }); 133 | 134 | 135 | test("Related properties :height", function() { 136 | const xml = ` 137 | <RaisedButton onPressed="event" :height="200"> 138 | <Text text="'Login'" /> 139 | </RaisedButton> 140 | `; 141 | 142 | const expected = ` 143 | SizedBox( 144 | height: 200, 145 | child: RaisedButton( 146 | onPressed: event, 147 | child: Text( 148 | 'Login', 149 | ), 150 | ), 151 | ) 152 | `; 153 | 154 | const generated = generateWidget(xml); 155 | assertEqual(generated, expected); 156 | }); 157 | 158 | 159 | test("Related properties :width & :height", function() { 160 | const xml = ` 161 | <RaisedButton onPressed="event" :width="100" :height="200"> 162 | <Text text="'Login'" /> 163 | </RaisedButton> 164 | `; 165 | 166 | const expected = ` 167 | SizedBox( 168 | height: 200, 169 | width: 100, 170 | child: RaisedButton( 171 | onPressed: event, 172 | child: Text( 173 | 'Login', 174 | ), 175 | ), 176 | ) 177 | `; 178 | 179 | const generated = generateWidget(xml); 180 | assertEqual(generated, expected); 181 | }); 182 | 183 | 184 | test("Related properties :width & :height with stream pipes", function() { 185 | const xml = ` 186 | <Text text="'Hello'" :width="streamVar | stream" :height="streamVar2 | stream" /> 187 | `; 188 | 189 | const expected = ` 190 | StreamBuilder( 191 | initialData: null, 192 | stream: streamVar2, 193 | builder: (BuildContext context, streamVar2Snapshot) { 194 | final streamVar2Value = streamVar2Snapshot.data; 195 | if (streamVar2Value == null) { 196 | return Container(width: 0, height: 0); 197 | } 198 | return StreamBuilder( 199 | initialData: null, 200 | stream: streamVar, 201 | builder: (BuildContext context, streamVarSnapshot) { 202 | final streamVarValue = streamVarSnapshot.data; 203 | if (streamVarValue == null) { 204 | return Container(width: 0, height: 0); 205 | } 206 | return SizedBox( 207 | height: streamVar2Value, 208 | width: streamVarValue, 209 | child: Text( 210 | 'Hello', 211 | ), 212 | ); 213 | }, 214 | ); 215 | }, 216 | ) 217 | `; 218 | 219 | const generated = generateWidget(xml); 220 | assertEqual(generated, expected); 221 | }); 222 | 223 | 224 | test("Related properties :width & :height with other pipes", function() { 225 | const xml = ` 226 | <Text text="'Hello'" :width="'test1' | translate" :height="'test2' | translate" /> 227 | `; 228 | 229 | const expected = ` 230 | SizedBox( 231 | height: _pipeProvider.transform(context, "translate", 'test2', []), 232 | width: _pipeProvider.transform(context, "translate", 'test1', []), 233 | child: Text( 234 | 'Hello', 235 | ), 236 | ) 237 | `; 238 | 239 | const generated = generateWidget(xml); 240 | assertEqual(generated, expected); 241 | }); 242 | 243 | test("priority test (should generate :topPriority -> :middlePriority -> :bottomPriority)", function() { 244 | const xml = `<Text :topPriority="1" :middlePriority="2" :bottomPriority="3" />`; 245 | 246 | const expected = ` 247 | TopPriorityWidget( 248 | 1, 249 | child: MiddlePriorityWidget( 250 | 2, 251 | child: BottomPriorityWidget( 252 | 3, 253 | child: Text( 254 | 255 | ), 256 | ), 257 | ), 258 | ) 259 | `; 260 | 261 | const generated = generateWidget(xml); 262 | assertEqual(generated, expected); 263 | }); 264 | 265 | test("priority test 2 (should generate :topPriority -> :middlePriority -> :bottomPriority)", function() { 266 | const xml = `<Text :bottomPriority="3" :topPriority="1" :middlePriority="2" />`; 267 | 268 | const expected = ` 269 | TopPriorityWidget( 270 | 1, 271 | child: MiddlePriorityWidget( 272 | 2, 273 | child: BottomPriorityWidget( 274 | 3, 275 | child: Text( 276 | 277 | ), 278 | ), 279 | ), 280 | ) 281 | `; 282 | 283 | const generated = generateWidget(xml); 284 | assertEqual(generated, expected); 285 | }); 286 | 287 | test("RaisedButton with :text & :padding)", function() { 288 | const xml = ` 289 | <RaisedButton :text="'Login'" :padding="5"> 290 | </RaisedButton> 291 | `; 292 | 293 | const expected = ` 294 | RaisedButton( 295 | child: Padding( 296 | padding: const EdgeInsets.all(5), 297 | child: Text( 298 | 'Login', 299 | ), 300 | ), 301 | ) 302 | `; 303 | 304 | const generated = generateWidget(xml); 305 | assertEqual(generated, expected); 306 | }); 307 | }); -------------------------------------------------------------------------------- /src/property-handlers/wrapper-property.ts: -------------------------------------------------------------------------------- 1 | import { CustomPropertyHandler, PropertyResolveResult } from "../providers/property-handler-provider"; 2 | import * as parseXml from '../parser/types'; 3 | import { WidgetModel, AttributeModel, PropertyModel } from '../models/models'; 4 | import { ConfigHandlerAndPropertyModel } from '../models/config'; 5 | import { PipeValueResolver } from "../resolvers/pipe-value-resolver"; 6 | import { ValueTransformersProvider } from "../providers/value-transformers-provider"; 7 | import { PropertyResolver } from "../resolvers/property-resolver"; 8 | 9 | export class WrapperPropertyHandler extends CustomPropertyHandler { 10 | protected getProperty(handler: string): ConfigHandlerAndPropertyModel { 11 | const property = this.properties.filter(a => a.handler === handler)[0]; 12 | if (property) { 13 | return property; 14 | } 15 | return { handler: '', targetProperty: '' }; 16 | } 17 | 18 | constructor( 19 | protected propertyResolver: PropertyResolver, 20 | protected properties: ConfigHandlerAndPropertyModel[], 21 | protected targetWidgetType: string, 22 | protected defaults?: { [name: string]: string }, 23 | priority: number = 100) { 24 | super(); 25 | this.priority = priority; 26 | } 27 | 28 | getRelatedProperties(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): string[] { 29 | return this.properties.map(a => a.handler); 30 | } 31 | 32 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 33 | return true; 34 | } 35 | 36 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 37 | const property = this.getProperty(attr.name); 38 | attr.value = property.value !== null && property.value !== undefined ? property.value : attr.value; 39 | const value = property.targetProperty ? ValueTransformersProvider.transform(attr.value, property.targetProperty, this.targetWidgetType) : attr.value; 40 | 41 | // if we have a custom property like (:if) we have to unwrap it 42 | // then make the (:if) as grandparent of the new widget (which is created in this method) 43 | // then make the original widget (widget) as a child of the new widget. 44 | // so in the next few lines we hold the grandparent (:if) 45 | // and the first non-custom widget for the last step below. 46 | 47 | let grandparentWidget: WidgetModel = null as any; 48 | let lastCustomParent: WidgetModel = null as any; 49 | if (widget.isCustom) { 50 | const getFirstNonCustomWidget = (w: WidgetModel): { customParent: WidgetModel, nonCustomChild: WidgetModel } => { 51 | if (w.wrappedWidgets && w.wrappedWidgets[0]) { 52 | if (w.isCustom && !w.wrappedWidgets[0].isCustom) { 53 | return { customParent: w, nonCustomChild: w.wrappedWidgets[0] }; 54 | } 55 | else { 56 | return getFirstNonCustomWidget(w.wrappedWidgets[0]); 57 | } 58 | } 59 | return null as any; 60 | }; 61 | grandparentWidget = widget; 62 | const result = getFirstNonCustomWidget(widget); 63 | widget = result.nonCustomChild; 64 | lastCustomParent = result.customParent; 65 | } 66 | 67 | const onWrapped: ((wrapper: WidgetModel) => void)[] = []; 68 | 69 | // create the new wrapper widget 70 | const creationResult = this.createWrapperWidget(widget, property.targetProperty, value, onWrapped); 71 | let wrapperWidget: WidgetModel = creationResult.wrapperWidget; 72 | 73 | // add related properties 74 | const relatedProperties = this.createRelatedProperties(element.attributes, property.targetProperty); 75 | creationResult.wrapperWidget.properties.push(...relatedProperties); 76 | 77 | // add properties' default values 78 | const defaultValues = this.createPropertiesDefaultValues(); 79 | creationResult.wrapperWidget.properties.push(...defaultValues); 80 | 81 | // apply binding on related properties 82 | for (const prop of relatedProperties) { 83 | const resolveResult = this.propertyResolver.pipeValueResolver.resolve(element, prop.name, prop.value as any, wrapperWidget, true); 84 | wrapperWidget = resolveResult.wrapperWidget || wrapperWidget; 85 | prop.value = resolveResult.value; 86 | } 87 | 88 | // resolve data-binding for current property 89 | if (typeof value === 'string') { 90 | const resolveResult = this.propertyResolver.pipeValueResolver.resolve(element, attr.name, value, wrapperWidget, true); 91 | 92 | if (creationResult.propertyToUpdateAfterBinding) { 93 | creationResult.propertyToUpdateAfterBinding.value = resolveResult.value; // update property value 94 | } 95 | 96 | if (resolveResult.wrapperWidget) { 97 | wrapperWidget = resolveResult.wrapperWidget || wrapperWidget; 98 | onWrapped.forEach(a => a && a(resolveResult.wrapperWidget as any)); 99 | } 100 | } 101 | 102 | 103 | // if we have a grandparent (which is ':if' in our example) then return it as a root 104 | // then add the new widget as a child of the last custom widget 105 | // (it could be the same (':if') or another nested custom) 106 | 107 | if (grandparentWidget) { 108 | lastCustomParent.wrappedWidgets = [wrapperWidget]; 109 | wrapperWidget = grandparentWidget; 110 | } 111 | 112 | return { extraData: null, wrapperWidget, value: attr.value, handled: true }; 113 | } 114 | 115 | protected createRelatedProperties(attributes: any, targetProperty: string): PropertyModel[] { 116 | const related: PropertyModel[] = this.properties 117 | .filter(prop => prop.targetProperty !== targetProperty && prop.handler in attributes) 118 | .map(prop => { 119 | const value = ValueTransformersProvider.transform(attributes[prop.handler], prop.handler, this.targetWidgetType); 120 | return { 121 | value, name: prop.targetProperty, dataType: 'object' 122 | } as PropertyModel; 123 | }); 124 | return related; 125 | } 126 | 127 | protected createWrapperWidget(widget: WidgetModel, targetProperty: string, value: string, onWrapped: ((wrapper: WidgetModel) => void)[]): { wrapperWidget: WidgetModel, propertyToUpdateAfterBinding: PropertyModel | null } { 128 | const propertyName = this.propertyResolver.isUnNamedParameter(targetProperty, this.targetWidgetType) ? '' : targetProperty; 129 | 130 | const wrapperWidget: WidgetModel = { 131 | controllers: [], 132 | vars: [], 133 | formControls: [], 134 | properties: [ 135 | { 136 | dataType: 'object', name: propertyName, value: value 137 | }, 138 | { 139 | dataType: 'widget', name: 'child', value: widget 140 | } 141 | ], 142 | type: this.targetWidgetType, 143 | wrappedWidgets: [widget], 144 | onResolved: [] 145 | }; 146 | 147 | return { wrapperWidget, propertyToUpdateAfterBinding: wrapperWidget.properties[0] }; 148 | } 149 | 150 | protected createPropertiesDefaultValues(): PropertyModel[] { 151 | const defaults = this.defaults || { }; 152 | return Object.keys(defaults).map(k => { 153 | return { 154 | name: k, 155 | value: defaults[k], 156 | dataType: 'object' 157 | } as PropertyModel; 158 | }); 159 | } 160 | } -------------------------------------------------------------------------------- /src/property-handlers/wrapper-stream-property.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Wrapper Stream Property Tests", function () { 4 | 5 | test("basic", function() { 6 | const xml = ` 7 | <Stack :stream="selectedTabStream:1:selectedTabValue"> 8 | <LatestPage :opacity="selectedTabValue == 0 ? 1: 0" /> 9 | <Text :opacity="selectedTabValue == 1 ? 1: 0" text="'Home'" /> 10 | <Text :opacity="selectedTabValue == 2 ? 1: 0" text="'Profile'" /> 11 | </Stack> 12 | `; 13 | 14 | const expected = ` 15 | StreamBuilder( 16 | initialData: 1, 17 | stream: selectedTabStream, 18 | builder: (BuildContext context, selectedTabStreamSnapshot) { 19 | final selectedTabValue = selectedTabStreamSnapshot.data; 20 | return Stack( 21 | children: [ 22 | Opacity( 23 | opacity: selectedTabValue == 0 ? 1: 0, 24 | child: LatestPage( 25 | 26 | ), 27 | ), 28 | Opacity( 29 | opacity: selectedTabValue == 1 ? 1: 0, 30 | child: Text( 31 | 'Home', 32 | ), 33 | ), 34 | Opacity( 35 | opacity: selectedTabValue == 2 ? 1: 0, 36 | child: Text( 37 | 'Profile', 38 | ), 39 | ), 40 | ], 41 | ); 42 | }, 43 | )`; 44 | 45 | const generated = generateWidget(xml); 46 | assertEqual(generated, expected); 47 | }); 48 | 49 | test("with another wrapper property like (margin)", function() { 50 | const xml = ` 51 | <Stack :stream="selectedTabStream:1:selectedTabValue" :margin="4"> 52 | <LatestPage :opacity="selectedTabValue == 0 ? 1: 0" /> 53 | <Text :opacity="selectedTabValue == 1 ? 1: 0" text="'Home'" /> 54 | <Text :opacity="selectedTabValue == 2 ? 1: 0" text="'Profile'" /> 55 | </Stack> 56 | `; 57 | 58 | const expected = ` 59 | StreamBuilder( 60 | initialData: 1, 61 | stream: selectedTabStream, 62 | builder: (BuildContext context, selectedTabStreamSnapshot) { 63 | final selectedTabValue = selectedTabStreamSnapshot.data; 64 | return Padding( 65 | padding: const EdgeInsets.all(4), 66 | child: Stack( 67 | children: [ 68 | Opacity( 69 | opacity: selectedTabValue == 0 ? 1: 0, 70 | child: LatestPage( 71 | 72 | ), 73 | ), 74 | Opacity( 75 | opacity: selectedTabValue == 1 ? 1: 0, 76 | child: Text( 77 | 'Home', 78 | ), 79 | ), 80 | Opacity( 81 | opacity: selectedTabValue == 2 ? 1: 0, 82 | child: Text( 83 | 'Profile', 84 | ), 85 | ), 86 | ], 87 | ), 88 | ); 89 | }, 90 | ) 91 | `; 92 | 93 | const generated = generateWidget(xml); 94 | assertEqual(generated, expected); 95 | }); 96 | }); -------------------------------------------------------------------------------- /src/property-handlers/wrapper-stream-property.ts: -------------------------------------------------------------------------------- 1 | import { WidgetModel, PropertyModel } from '../models/models'; 2 | import { WrapperPropertyHandler } from "./wrapper-property"; 3 | import { makeVariableName } from "../utils"; 4 | import { PropertyResolver } from '../resolvers/property-resolver'; 5 | 6 | export class WrapperStreamPropertyHandler extends WrapperPropertyHandler { 7 | constructor(propertyResolver: PropertyResolver) { 8 | super(propertyResolver,[{ handler: ':stream', targetProperty: 'stream' }], 'StreamBuilder', undefined, 9999); // top priority but less than Consumer 9 | } 10 | 11 | protected createWrapperWidget(widget: WidgetModel, targetProperty: string, value: string, onWrapped: ((wrapper: WidgetModel) => void)[]): { wrapperWidget: WidgetModel, propertyToUpdateAfterBinding: PropertyModel | null } { 12 | const parts = value.split(':'); 13 | const streamName = parts[0]; 14 | const initialValue = parts[1]; 15 | let resultVarName = parts[2]; 16 | const parameterNamePrefix = makeVariableName('', streamName); 17 | const snapshotVarName = parameterNamePrefix + 'Snapshot'; 18 | resultVarName = resultVarName || `${parameterNamePrefix}Value`; 19 | 20 | const wrapperWidget: WidgetModel = { 21 | controllers: [], 22 | vars: [], 23 | formControls: [], 24 | properties: [ 25 | { 26 | dataType: 'object', value: `${streamName}`, name: 'stream' 27 | }, 28 | { 29 | dataType: 'object', value: `${initialValue ? initialValue : 'null'}`, name: 'initialData' 30 | }, 31 | { 32 | dataType: 'function', 33 | name: 'builder', 34 | value: '', 35 | extraData: { 36 | parameters: [ 37 | { name: 'context', type: 'BuildContext' }, 38 | { name: `${snapshotVarName}`, type: '' } 39 | ], 40 | logic: [ 41 | `final ${resultVarName} = ${snapshotVarName}.data;` 42 | ], 43 | addReturn: true 44 | } 45 | } 46 | ], 47 | type: this.targetWidgetType, 48 | wrappedWidgets: [widget], 49 | onResolved: [] 50 | }; 51 | return { wrapperWidget, propertyToUpdateAfterBinding: null }; 52 | } 53 | } -------------------------------------------------------------------------------- /src/providers/property-handler-provider.ts: -------------------------------------------------------------------------------- 1 | import * as parseXml from '../parser/types'; 2 | import { WidgetModel, ExtraDataModel, AttributeModel, PropertyModel, AttributeInfo } from '../models/models'; 3 | 4 | export abstract class CustomPropertyHandler { 5 | priority: number = 100; 6 | 7 | // 8 | // language features' properties 9 | // 10 | isElement = false; 11 | elementAttributes: AttributeInfo[]; 12 | valueSnippet: string; 13 | documentation: string; 14 | 15 | 16 | getRelatedProperties(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): string[] { 17 | return []; 18 | } 19 | 20 | canResolvePropertyElement(): boolean { 21 | return false; 22 | } 23 | 24 | resolvePropertyElement(element: parseXml.Element, widgetResolveResult: WidgetResolveResult, parent: parseXml.Element, parentChildren: parseXml.Element[], resolveWidget: (element: parseXml.Element, parent: parseXml.Element) => WidgetResolveResult): WidgetModel | null { 25 | return null; 26 | } 27 | 28 | canResolve(element: parseXml.Element, handlerProperty: string, widget: WidgetModel): boolean { 29 | return false; 30 | } 31 | 32 | resolve(element: parseXml.Element, attr: AttributeModel, widget: WidgetModel): PropertyResolveResult { 33 | return { 34 | wrapperWidget: null, 35 | value: attr.value, 36 | handled: false, 37 | extraData: null 38 | }; 39 | } 40 | 41 | canGenerate(widget: WidgetModel): boolean { 42 | return false; 43 | } 44 | 45 | generate(widget: WidgetModel, tabsLevel: number, 46 | generateChildWidgetCode: (widget: WidgetModel, tabsLevel: number) => string, 47 | generatePropertyCode: (widget: WidgetModel, property: PropertyModel, tabsLevel: number) => string): string { 48 | return ''; 49 | } 50 | } 51 | 52 | export class PropertyHandlerProvider { 53 | private readonly handlers: { [name: string]: CustomPropertyHandler } = {}; 54 | 55 | register(name: string | string[], handler: CustomPropertyHandler) { 56 | if (name instanceof Array) { 57 | name.forEach(n => this.handlers[n] = handler); 58 | } 59 | else { 60 | this.handlers[name] = handler; 61 | } 62 | } 63 | 64 | getAll(): { [name: string]: CustomPropertyHandler } { 65 | return this.handlers; 66 | } 67 | 68 | get(name: string): CustomPropertyHandler { 69 | return this.handlers[name]; 70 | } 71 | 72 | remove(name: string | string[]) { 73 | if (name instanceof Array) { 74 | name.forEach(n => delete this.handlers[n]); 75 | } 76 | else { 77 | delete this.handlers[name]; 78 | } 79 | } 80 | } 81 | 82 | export interface PropertyResolveResult { 83 | wrapperWidget: WidgetModel | null; 84 | extraData: ExtraDataModel | null; 85 | value: string; 86 | handled: boolean; 87 | } 88 | 89 | export interface WidgetResolveResult { 90 | widget: WidgetModel; 91 | isPropertyElement: boolean; 92 | propertyElement: string; 93 | propertyElementProperties: any[]; 94 | } -------------------------------------------------------------------------------- /src/providers/value-transformers-provider.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface IValueTransformer { 3 | transform(originalValue: string, name: string, widgetType: string): ValueTransformResult; 4 | } 5 | 6 | export interface ValueTransformResult { 7 | handled: boolean; 8 | value: string; 9 | propertyType: string; 10 | } 11 | 12 | export class ValueTransformersProvider { 13 | private readonly resolvers: { [name: string]: IValueTransformer } = {}; 14 | private static instance: ValueTransformersProvider; 15 | 16 | constructor() { 17 | ValueTransformersProvider.instance = this; 18 | } 19 | 20 | register(name: string | string[], resolver: IValueTransformer) { 21 | if (name instanceof Array) { 22 | name.forEach(n => this.resolvers[n] = resolver); 23 | } 24 | else { 25 | this.resolvers[name] = resolver; 26 | } 27 | } 28 | 29 | get(name: string): IValueTransformer { 30 | return this.resolvers[name]; 31 | } 32 | 33 | static transform(value: string, name: string, widgetType: string): string { 34 | const valueTransformer = ValueTransformersProvider.instance.get(name); 35 | let valueTransformResult: ValueTransformResult = { } as any; 36 | if (valueTransformer) { 37 | valueTransformResult = valueTransformer.transform(value, name, widgetType); 38 | if (valueTransformResult.handled) { 39 | return valueTransformResult.value; 40 | } 41 | } 42 | 43 | return value; 44 | } 45 | } -------------------------------------------------------------------------------- /src/resolvers/property-element.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("PropertyElement Tests", function () { 4 | 5 | test("will generate a property for lowercase-element", function() { 6 | const xml = ` 7 | <Container> 8 | <child> 9 | <Column /> 10 | </child> 11 | </Container>`; 12 | 13 | const expected = ` 14 | Container( 15 | child: Column( 16 | children: [ 17 | ], 18 | ), 19 | ) 20 | `; 21 | 22 | const generated = generateWidget(xml); 23 | assertEqual(generated, expected); 24 | }); 25 | 26 | test("lowercase-element with owner widget type", function() { 27 | const xml = ` 28 | <Container> 29 | <Container.child> 30 | <Column /> 31 | </Container.child> 32 | </Container>`; 33 | 34 | const expected = ` 35 | Container( 36 | child: Column( 37 | children: [ 38 | ], 39 | ), 40 | ) 41 | `; 42 | 43 | const generated = generateWidget(xml); 44 | assertEqual(generated, expected); 45 | }); 46 | }); -------------------------------------------------------------------------------- /src/resolvers/use.test.ts: -------------------------------------------------------------------------------- 1 | import { generateWidget, assertEqual } from '../test/shared'; 2 | 3 | suite("Use Tests", function () { 4 | 5 | test("will generate ListView.builder()", function() { 6 | const xml = `<ListView :use="builder"> 7 | </ListView>`; 8 | 9 | const expected = ` 10 | ListView.builder(children:[],) 11 | `; 12 | 13 | const generated = generateWidget(xml); 14 | assertEqual(generated, expected); 15 | }); 16 | }); -------------------------------------------------------------------------------- /src/resolvers/util.ts: -------------------------------------------------------------------------------- 1 | import { WidgetModel, PropertyModel } from "../models/models"; 2 | 3 | 4 | export function removeDuplicatedBuilders(widget: WidgetModel, parent: WidgetModel | WidgetModel[] | null, parentType: 'wrappedWidgets' | 'widget' | 'widgetList', buildersCache: any) { 5 | if (!widget) { 6 | return; 7 | } 8 | 9 | // find and remove duplicated StreamBuilder & FutureBuilder that have same value. 10 | let detach = false; 11 | 12 | if (widget.type === 'StreamBuilder' || widget.type === 'FutureBuilder') { 13 | const builderValue = widget.properties[0].value as string; 14 | if (builderValue in buildersCache && buildersCache[builderValue] && 15 | buildersCache[builderValue].type === widget.type && 16 | buildersCache[builderValue].widget.id !== widget.id && 17 | isParentOf(buildersCache[builderValue].widget, widget)) { 18 | detach = true; 19 | } 20 | else { 21 | // register as visited 22 | buildersCache[builderValue] = { type: widget.type, widget: widget }; 23 | } 24 | } 25 | 26 | // visit wrappedWidgets 27 | for (const child of widget.wrappedWidgets) { 28 | removeDuplicatedBuilders(child, widget.wrappedWidgets, 'wrappedWidgets', buildersCache); 29 | } 30 | 31 | // visit child property 32 | const childWidgetProp = widget.properties.filter(a => a.dataType === 'widget')[0]; 33 | if (childWidgetProp) { 34 | removeDuplicatedBuilders(childWidgetProp.value as any, widget, 'widget', buildersCache); 35 | } 36 | 37 | // visit children property 38 | const childrenWidgetsProp = widget.properties.filter(a => a.dataType === 'widgetList')[0]; 39 | if (childrenWidgetsProp) { 40 | for (const child of childrenWidgetsProp.value as any[]) { 41 | removeDuplicatedBuilders(child, widget, 'widgetList', buildersCache); 42 | } 43 | } 44 | 45 | // visit propertyElement(s) 46 | const propertyElementsProps = widget.properties.filter(a => a.dataType === 'propertyElement'); 47 | for (const propertyElementProp of propertyElementsProps) { 48 | const propertyValue = (propertyElementProp.value as any); 49 | if (propertyValue) { 50 | if (propertyValue.dataType === 'widget') { 51 | removeDuplicatedBuilders(propertyValue.value as any, widget, 'widget', buildersCache); 52 | } 53 | else if (propertyValue.dataType === 'widgetList') { 54 | for (const child of propertyValue.value as any[]) { 55 | removeDuplicatedBuilders(child, widget, 'widgetList', buildersCache); 56 | } 57 | } 58 | } 59 | } 60 | 61 | // detach builder from the hierarchy. 62 | // this operation should be last step 63 | if (detach && parent) { 64 | if (parentType === 'widget') { 65 | const childWidgetProp = (parent as WidgetModel).properties.filter(a => a.dataType === 'widget')[0]; 66 | if (childWidgetProp) { 67 | childWidgetProp.value = widget.wrappedWidgets[0]; 68 | } 69 | } 70 | else if (parentType === 'widgetList') { 71 | const childrenWidgetsProp = (parent as WidgetModel).properties.filter(a => a.dataType === 'widgetList')[0]; 72 | if (childrenWidgetsProp) { 73 | const index = (childrenWidgetsProp.value as any[]).findIndex(a => a === widget); 74 | (childrenWidgetsProp.value as any[])[index] = widget.wrappedWidgets[0]; 75 | } 76 | } 77 | else if (parentType === 'wrappedWidgets' && parent instanceof Array) { 78 | const index = parent.findIndex(a => a === widget); 79 | parent[index] = widget.wrappedWidgets[0]; 80 | } 81 | } 82 | } 83 | 84 | function isParentOf(parent: WidgetModel, child: WidgetModel): boolean { 85 | const res = getStreamBuilderRecursively(parent, child.id); 86 | return !!res; 87 | } 88 | 89 | // 90 | // we check if there is a stream with streamId in its hierarchy, that includes: 91 | // - all properties: widget, widgetList and propertyElement 92 | // - wrapperWidgets 93 | // 94 | function getStreamBuilderRecursively(parentWidget: WidgetModel, streamId: any): PropertyModel /*| WidgetModel[]*/ | null { 95 | if (parentWidget.id === streamId) { 96 | return true as any; 97 | } 98 | 99 | const getThroughWidgets = (widget: WidgetModel | WidgetModel[]): PropertyModel | null => { 100 | if (widget) { 101 | if (widget instanceof Array) { 102 | for (const w of widget) { 103 | const prop = getStreamBuilderRecursively(w, streamId); 104 | if (prop) { 105 | return prop; 106 | } 107 | } 108 | } 109 | else { 110 | const prop = getStreamBuilderRecursively(widget, streamId); 111 | if (prop) { 112 | return prop; 113 | } 114 | } 115 | } 116 | return null; 117 | }; 118 | 119 | let childProp = parentWidget.properties.filter(a => a.dataType === 'widget' || a.dataType === 'widgetList')[0]; 120 | if (childProp) { 121 | if ((childProp.value as WidgetModel).id === streamId) { 122 | return childProp; 123 | } 124 | 125 | const res = getThroughWidgets(childProp.value as any); 126 | if (res) { 127 | if (res as any === true) { 128 | return res; 129 | } 130 | if ((res.value as WidgetModel).id === streamId) { 131 | return res; 132 | } 133 | } 134 | } 135 | else { 136 | const propertyElementProps = parentWidget.properties.filter(a => a.dataType === 'propertyElement'); 137 | for (const p of propertyElementProps) { 138 | const w = (p.value as any).value as WidgetModel; 139 | if (w.id === streamId) { 140 | return p.value as any; 141 | } 142 | 143 | let prop = getThroughWidgets(w); 144 | if (prop) { 145 | return prop; 146 | } 147 | // else if (p.extraData && p.extraData.properties) { 148 | // prop = this.getStreamBuilderRecursively(p.extraData as any, streamId); 149 | // if (prop) { 150 | // return prop; 151 | // } 152 | // } 153 | } 154 | } 155 | 156 | if (parentWidget.wrappedWidgets) { 157 | for (const w of parentWidget.wrappedWidgets) { 158 | if (streamId === w.id) { 159 | return parentWidget.wrappedWidgets as any; 160 | } 161 | 162 | const prop = getStreamBuilderRecursively(w, streamId); 163 | if (prop) { 164 | return prop; 165 | } 166 | } 167 | } 168 | 169 | return null; 170 | } -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testsRoot: string, clb: (error: Error, failures?: number) => void): void 9 | // that the extension host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by configuring the test runner below 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options 17 | // for more info 18 | testRunner.configure({ 19 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 20 | useColors: true // colored output from test results 21 | }); 22 | 23 | module.exports = testRunner; -------------------------------------------------------------------------------- /src/test/shared.ts: -------------------------------------------------------------------------------- 1 | import { ParseXml } from "../parser/parser"; 2 | import { WidgetResolver } from "../resolvers/widget-resolver"; 3 | import { PropertyHandlerProvider } from "../providers/property-handler-provider"; 4 | import assert = require("assert"); 5 | import { ValueTransformersProvider } from "../providers/value-transformers-provider"; 6 | import { WidgetCodeGenerator } from "../generators/widget-generator"; 7 | import { Config } from "../models/config"; 8 | import { registerBuiltInValueTransformers, registerBuiltInPropertyHandlers } from "../builtin-handlers"; 9 | import { WrapperPropertyHandler } from "../property-handlers/wrapper-property"; 10 | import { PipeValueResolver } from "../resolvers/pipe-value-resolver"; 11 | import { PropertyResolver } from "../resolvers/property-resolver"; 12 | 13 | const config: Config = {}; 14 | const pipeValueResolver = new PipeValueResolver(); 15 | const propertyHandlersProvider = new PropertyHandlerProvider(); 16 | const propertyResolver = new PropertyResolver(config, propertyHandlersProvider, pipeValueResolver); 17 | const valueTransformersProvider = new ValueTransformersProvider(); 18 | const resolver = new WidgetResolver(config, propertyHandlersProvider, propertyResolver); 19 | const widgetCodeGenerator = new WidgetCodeGenerator(propertyHandlersProvider); 20 | const parser: ParseXml = new ParseXml(); 21 | 22 | 23 | registerBuiltInPropertyHandlers(propertyHandlersProvider, propertyResolver); 24 | registerBuiltInValueTransformers(valueTransformersProvider); 25 | 26 | 27 | propertyHandlersProvider.register(':topCenter', new WrapperPropertyHandler(propertyResolver, [{ handler: ':topCenter', targetProperty: 'alignment', value: 'Alignment.topCenter' }], 'Align')); 28 | propertyHandlersProvider.register(':testText', new WrapperPropertyHandler(propertyResolver, [{ handler: ':testText', targetProperty: '' }], 'Text')); 29 | propertyHandlersProvider.register(':topPriority', new WrapperPropertyHandler(propertyResolver, [{ handler: ':topPriority', targetProperty: '' }], 'TopPriorityWidget', undefined, 1000)); 30 | propertyHandlersProvider.register(':middlePriority', new WrapperPropertyHandler(propertyResolver, [{ handler: ':middlePriority', targetProperty: '' }], 'MiddlePriorityWidget', undefined, 500)); 31 | propertyHandlersProvider.register(':bottomPriority', new WrapperPropertyHandler(propertyResolver, [{ handler: ':bottomPriority', targetProperty: '' }], 'BottomPriorityWidget', undefined, 10)); 32 | 33 | 34 | 35 | export function generateWidget(xml: string): string { 36 | xml = `<StatefulWidget name="TestPage" controller="TestController">${xml}</StatefulWidget>`; 37 | const xmlDoc = parser.parse(xml); 38 | const resolvedWidget = resolver.resolve(xmlDoc); 39 | const layoutDart = widgetCodeGenerator.generateWidgetCode(resolvedWidget.rootChild, 0); 40 | return layoutDart; 41 | } 42 | 43 | 44 | export function assertEqual(actual: string, expected: string) { 45 | expected = expected.replace(/\s/ig, ''); 46 | actual = actual.replace(/\s/ig, ''); 47 | assert.equal(actual, expected); 48 | } 49 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { PropertyModel, WidgetModel } from "./models/models"; 2 | 3 | 4 | export function extractForLoopParams(value: string) { 5 | const statements = value.split(','); 6 | const indexName = statements.length > 1 ? statements[0].trim() : ''; 7 | const segments = (statements.length > 1 ? statements[1] : statements[0]).trim().split(' of '); 8 | const { itemName, typeName } = extractVarAndType(segments[0]); 9 | const listName = segments[1]; 10 | return { listName, indexName, itemName, typeName }; 11 | } 12 | 13 | export function extractVarAndType(value: string): { itemName: string, typeName: string } { 14 | const statements = value.split(' '); 15 | if (statements.length === 2) { 16 | return { itemName: statements[1], typeName: statements[0] }; 17 | } 18 | else { 19 | return { itemName: statements[0], typeName: '' }; 20 | } 21 | } 22 | 23 | export function spaceAfter(text: string) { 24 | return text ? text + ' ' : ''; 25 | } 26 | 27 | export function makeTabs(tabsLevel: number) { 28 | return [...new Array(tabsLevel + 2)].map(_ => ' ').join(''); 29 | } 30 | 31 | export function makeVariableName(name: string, value: string) { 32 | // changing this will break most of the tests 33 | 34 | const res = value 35 | // replace all non-alphnumeric with underscore 36 | .replace(/[^a-zA-Z0-9_]/ig, '_') 37 | // replace multiple underscores with one 38 | .replace(/__+/g, '_') 39 | // uppercase first char after underscore 40 | .replace(/_([a-z])/g, g => g[1].toUpperCase()) 41 | // remove all underscores 42 | .replace(/_/g, ''); 43 | // lowercase first char 44 | return res[0].toLowerCase() + res.substring(1); 45 | } 46 | 47 | export function makePipeUniqueName(data: { pipes: any[], value: string }) { 48 | // changing this will not break any tests 49 | 50 | const names = (data.pipes as any[]).map(a => a.name).join('_'); 51 | 52 | const res = data.value 53 | // replace all non-alphnumeric with underscore 54 | .replace(/[^a-zA-Z0-9_]/ig, '_'); 55 | 56 | return names + res; 57 | } 58 | 59 | export function sortProperties(a: PropertyModel, b: PropertyModel) { 60 | // changing this will break most of the tests 61 | 62 | // un-named will be first 63 | if (!a.name && b.name) { 64 | return -1; 65 | } 66 | else if (a.name && !b.name) { 67 | return 1; 68 | } 69 | 70 | // then widgets (alphabetaclly) 71 | const aIsWidget = ['widget', 'widgetList', 'function', 'propertyElement'].filter(t => t === a.dataType).length === 1; 72 | const bIsWidget = ['widget', 'widgetList', 'function', 'propertyElement'].filter(t => t === b.dataType).length === 1; 73 | if (!aIsWidget && bIsWidget) { 74 | return -1; 75 | } 76 | else if (aIsWidget && !bIsWidget) { 77 | return 1; 78 | } 79 | 80 | // then non-widget (alphabetaclly) 81 | if (a.name > b.name) { 82 | return 1; 83 | } 84 | else if (a.name < b.name) { 85 | return -1; 86 | } 87 | 88 | return 0; 89 | } 90 | 91 | export function getUniqueBy(array: any[], key: (val: any) => any) { 92 | return [ 93 | ...new Map( 94 | array.map(x => [key(x), x]) 95 | ).values() 96 | ]; 97 | } 98 | 99 | 100 | export function findWidgetByName(name: string, widget: WidgetModel): WidgetModel { 101 | if (name === widget.type) { 102 | return widget; 103 | } 104 | return findWidgetByName(name, widget.wrappedWidgets[0]); 105 | } -------------------------------------------------------------------------------- /src/value-transformers/color.ts: -------------------------------------------------------------------------------- 1 | import { IValueTransformer, ValueTransformResult } from "../providers/value-transformers-provider"; 2 | 3 | 4 | export class ColorValueTransformer implements IValueTransformer { 5 | transform(originalValue: string, name: string, widgetType: string): ValueTransformResult { 6 | let value; 7 | let handled = false; 8 | 9 | if (originalValue.startsWith('#')) { 10 | const r = originalValue.substr(1, 2); 11 | const g = originalValue.substr(3, 2); 12 | const b = originalValue.substr(5, 2); 13 | let a = originalValue.substr(7, 2); 14 | if (!a && a !== '0') { 15 | a = 'ff'; 16 | } 17 | value = `Color.fromARGB(${parseInt(a, 16)}, ${parseInt(r, 16)}, ${parseInt(g, 16)}, ${parseInt(b, 16)})`; 18 | handled = true; 19 | } 20 | else if (!originalValue.startsWith('Colors.') && 21 | !originalValue.startsWith('ctrl.') && 22 | /^[a-zA-Z0-9\.\s*]+$/ig.test(originalValue) && 23 | !originalValue.startsWith('widget.')) { 24 | const dotsCount = (originalValue.match(/\./g) || []).length; 25 | if (dotsCount === 1 && (originalValue.indexOf('.shade') > -1 || originalValue.indexOf('.with') > -1 && originalValue.indexOf('(') > -1) ||// e.g. accept red, red.shaded100, red.with*() 26 | dotsCount === 0) { 27 | value = `Colors.${originalValue}`; 28 | handled = true; 29 | } 30 | } 31 | else { 32 | value = originalValue; 33 | } 34 | 35 | return { 36 | handled: handled, 37 | propertyType: 'object', 38 | value: value 39 | }; 40 | } 41 | } -------------------------------------------------------------------------------- /src/value-transformers/decoration.ts: -------------------------------------------------------------------------------- 1 | import { IValueTransformer, ValueTransformResult } from "../providers/value-transformers-provider"; 2 | 3 | 4 | export class DecorationValueTransformer implements IValueTransformer { 5 | transform(originalValue: string, name: string, widgetType: string): ValueTransformResult { 6 | let value = originalValue; 7 | let handled = false; 8 | 9 | if (widgetType === 'Text') { 10 | const values = originalValue.split(' '); 11 | value = `TextDecoration.combine(${values.map(a => 'TextDecoration.' + a).join(', ')}`; 12 | handled = true; 13 | } 14 | else { 15 | } 16 | 17 | return { 18 | handled: handled, 19 | propertyType: 'object', 20 | value: value 21 | }; 22 | } 23 | } -------------------------------------------------------------------------------- /src/value-transformers/edge-insets.ts: -------------------------------------------------------------------------------- 1 | import { IValueTransformer, ValueTransformResult } from "../providers/value-transformers-provider"; 2 | 3 | 4 | export class EdgeInsetsValueTransformer implements IValueTransformer { 5 | transform(originalValue: string, name: string, widgetType: string): ValueTransformResult { 6 | let value = originalValue; 7 | 8 | if (/^[0-9\.\s*]*$/ig.test(originalValue)) { 9 | const segments = originalValue.split(' '); 10 | 11 | if (segments.length === 1) { 12 | value = `all(${originalValue})`; 13 | } 14 | else if (segments.length === 2) { 15 | value = `symmetric(vertical: ${segments[0]}, horizontal: ${segments[1]})`; 16 | } 17 | else if (segments.length === 3) { 18 | value = `fromLTRB(${segments[1]}, ${segments[0]}, ${segments[1]}, ${segments[2]})`; 19 | } 20 | else if (segments.length === 4) { 21 | value = `fromLTRB(${segments[3]}, ${segments[0]}, ${segments[1]}, ${segments[2]})`; 22 | } 23 | else { 24 | value = 'all(0)'; 25 | } 26 | 27 | value = 'const EdgeInsets.' + value; 28 | } 29 | 30 | return { 31 | handled: true, 32 | propertyType: 'object', 33 | value: value 34 | }; 35 | } 36 | } -------------------------------------------------------------------------------- /src/value-transformers/enum.ts: -------------------------------------------------------------------------------- 1 | import { IValueTransformer, ValueTransformResult } from "../providers/value-transformers-provider"; 2 | 3 | 4 | export class EnumValueTransformer implements IValueTransformer { 5 | enumName: string; 6 | 7 | constructor(enumName: string) { 8 | this.enumName = enumName; 9 | } 10 | 11 | transform(originalValue: string, name: string, widgetType: string): ValueTransformResult { 12 | const value = this.resolveEnumValue(this.enumName, originalValue); 13 | return { 14 | handled: true, 15 | propertyType: 'object', 16 | value: value 17 | }; 18 | } 19 | 20 | private resolveEnumValue(enumName: string, value: string): string { 21 | if (/^[a-zA-Z0-9_]+$/.test(value)) { 22 | return `${enumName}.${value}`; 23 | } 24 | return value; 25 | } 26 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true, /* enable all strict type-checking options */ 12 | "strictNullChecks": false 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "curly": false, 4 | "no-console": false, 5 | "max-classes-per-file": false, 6 | "interface-name": false, 7 | "member-ordering": false, 8 | "max-line-length": false, 9 | "ordered-imports": false, 10 | "no-shadowed-variable": false 11 | }, 12 | "defaultSeverity": "warning" 13 | } 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'extension.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]' 18 | }, 19 | devtool: 'source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; --------------------------------------------------------------------------------