├── lib ├── src │ ├── js │ │ ├── process_elements.sh │ │ └── analyze.js │ ├── common.dart │ ├── html_element_names.dart │ ├── config.dart │ ├── ast.dart │ └── codegen.dart └── generate_dart_api.dart ├── codereview.settings ├── e2e_test ├── pubspec.yaml ├── test │ ├── mock_file.dart │ ├── expected │ │ ├── example_element_with_behavior.dart │ │ ├── example_multi_deep_behavior.dart │ │ ├── example_element_with_deep_behavior.dart │ │ ├── example_multi_behavior.dart │ │ ├── example_behavior.dart │ │ └── example_element.dart │ └── e2e_test.dart ├── behavior_config.yaml └── lib │ └── src │ ├── example_element_with_behavior │ └── example_element_with_behavior.html │ ├── example_element_with_deep_behavior │ └── example_element_with_deep_behavior.html │ ├── example_multi_deep_behavior │ └── example_multi_deep_behavior.html │ ├── example_multi_behavior │ └── example_multi_behavior.html │ ├── example_behavior │ └── example_behavior.html │ └── example_element │ └── example_element.html ├── .gitignore ├── CONTRIBUTING.md ├── pubspec.yaml ├── README.md ├── LICENSE ├── bin └── update.dart └── CHANGELOG.md /lib/src/js/process_elements.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | node packages/custom_element_apigen/src/js/analyze.js $* 4 | -------------------------------------------------------------------------------- /codereview.settings: -------------------------------------------------------------------------------- 1 | CODE_REVIEW_SERVER: https://codereview.chromium.org/ 2 | VIEW_VC: https://github.com/dart-lang/custom-element-apigen/commit/ 3 | CC_LIST: 4 | -------------------------------------------------------------------------------- /e2e_test/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: e2e_test 2 | version: 0.0.0 3 | author: Polymer.dart Authors 4 | dependencies: 5 | custom_element_apigen: 6 | path: ../ 7 | polymer_interop: ^1.0.0-rc.1 8 | dev_dependencies: 9 | test: ^0.12.0 10 | environment: 11 | sdk: '>=1.6.0 <2.0.0' 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following directories created by pub. 2 | build/ 3 | packages 4 | **/.packages 5 | 6 | # Or the files created by dart2js. 7 | *.dart.js 8 | *.dart.precompiled.js 9 | *.js_ 10 | *.js.deps 11 | *.js.map 12 | *.sw? 13 | .idea/ 14 | .pub 15 | node_modules 16 | 17 | # Include when developing application packages. 18 | pubspec.lock 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ### Sign our Contributor License Agreement (CLA) 4 | 5 | Even for small changes, we ask that you please sign the CLA electronically 6 | [here](https://developers.google.com/open-source/cla/individual). 7 | The CLA is necessary because you own the copyright to your changes, even 8 | after your contribution becomes part of our codebase, so we need your permission 9 | to use and distribute your code. You can find more details 10 | [here](https://code.google.com/p/dart/wiki/Contributing). 11 | -------------------------------------------------------------------------------- /e2e_test/test/mock_file.dart: -------------------------------------------------------------------------------- 1 | library e2e_test.mock_file; 2 | 3 | import 'dart:convert'; 4 | import 'dart:io'; 5 | 6 | class MockFile implements File { 7 | static List createdFiles = []; 8 | String contents = ''; 9 | final String path; 10 | 11 | MockFile(this.path); 12 | 13 | void createSync({bool recursive: false}) { 14 | createdFiles.add(this); 15 | } 16 | 17 | void writeAsStringSync(String contents, {FileMode mode: FileMode.WRITE, 18 | Encoding encoding: UTF8, bool flush: false}) { 19 | this.contents += contents; 20 | } 21 | 22 | noSuchMethod(_) { 23 | throw new UnimplementedError(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: custom_element_apigen 2 | version: 0.2.2+1 3 | author: Polymer.dart Authors 4 | description: Generates Dart API for JS Custom Elements 5 | homepage: https://github.com/dart-lang/custom-element-apigen 6 | dependencies: 7 | html: ">=0.12.0 <0.13.0" 8 | path: ">=1.0.0 <2.0.0" 9 | test: ^0.12.0 10 | yaml: ">=1.0.0 <3.0.0" 11 | # TODO(sigmund): maybe remove these dependencies? They are required only for 12 | # the generated code. We could make the codegen ensure that it is listed in 13 | # the package pubspec instead. 14 | polymer_interop: ^1.0.0-rc.1 15 | web_components: ^0.12.0 16 | environment: 17 | sdk: '>=1.8.0 <2.0.0' 18 | -------------------------------------------------------------------------------- /e2e_test/behavior_config.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for running tool/generate_dart_api.dart 2 | files_to_generate: 3 | - example_behavior/example_behavior.html: 4 | type_overrides: 5 | ExampleBehavior: 6 | behaviorWrongTypeProperty: 7 | type: Number 8 | - example_element/example_element.html: 9 | type_overrides: 10 | ExampleElement: 11 | elementWrongTypeProperty: 12 | type: Number 13 | - example_element_with_behavior/example_element_with_behavior.html 14 | - example_multi_behavior/example_multi_behavior.html 15 | - example_multi_deep_behavior/example_multi_deep_behavior.html 16 | - example_element_with_deep_behavior/example_element_with_deep_behavior.html 17 | -------------------------------------------------------------------------------- /e2e_test/test/expected/example_element_with_behavior.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 2 | 3 | /// Dart API for the polymer element `example_element_with_behavior`. 4 | @HtmlImport('example_element_with_behavior_nodart.html') 5 | library e2e_test.lib.src.example_element_with_behavior.example_element_with_behavior; 6 | 7 | import 'dart:html'; 8 | import 'dart:js' show JsArray, JsObject; 9 | import 'package:web_components/web_components.dart'; 10 | import 'package:polymer_interop/polymer_interop.dart'; 11 | import 'example_behavior.dart'; 12 | 13 | /// An example element with a behavior. 14 | @CustomElementProxy('example-element-with-behavior') 15 | class ExampleElementWithBehavior extends HtmlElement with CustomElementProxyMixin, PolymerBase, ExampleBehavior { 16 | ExampleElementWithBehavior.created() : super.created(); 17 | factory ExampleElementWithBehavior() => new Element.tag('example-element-with-behavior'); 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Elements APIGen 2 | 3 | This package contains a tool that lets you wrap custom elements written with 4 | polymer.js and provide a Dart API for them. 5 | 6 | The tool assumes that the JavaScript code was packaged using bower and follows 7 | bower packages conventions. 8 | 9 | To use it, you need to: 10 | * Install node/npm. 11 | * Globally install `bower` and `hydrolysis` npm packages using 12 | `npm install -g `. 13 | * configure bower to put the packages under `lib/src/` instead of the default 14 | `bower_components`. For exmaple, with a `.bowerrc` file as follows: 15 | 16 | { 17 | "directory": "lib/src" 18 | } 19 | 20 | * setup your `bower.json` file 21 | * install packages via `bower install` 22 | * (one time) create a configuration file for this tool 23 | * run the tool via `pub run custom_element_apigen:update configfile.yaml` 24 | 25 | There is not much documentation written for this tool. You can find examples of 26 | how this tool is used in the [polymer_elements][1] package. 27 | 28 | [1]: https://github.com/dart-lang/polymer_elements/ 29 | -------------------------------------------------------------------------------- /e2e_test/test/expected/example_multi_deep_behavior.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 2 | 3 | /// Dart API for the polymer element `example_multi_deep_behavior`. 4 | @HtmlImport('example_multi_deep_behavior_nodart.html') 5 | library e2e_test.lib.src.example_multi_deep_behavior.example_multi_deep_behavior; 6 | 7 | import 'dart:html'; 8 | import 'dart:js' show JsArray, JsObject; 9 | import 'package:web_components/web_components.dart'; 10 | import 'package:polymer_interop/polymer_interop.dart'; 11 | import 'example_multi_behavior.dart'; 12 | import 'example_behavior.dart'; 13 | 14 | /// This is an example behavior! 15 | @BehaviorProxy(const ['Polymer', 'ExampleMultiDeepBehavior']) 16 | abstract class ExampleMultiDeepBehavior implements CustomElementProxyMixin, ExampleMultiBehavior { 17 | 18 | /// A public property created with the properties descriptor. 19 | get yetAnotherPublicProperty => jsElement[r'yetAnotherPublicProperty']; 20 | set yetAnotherPublicProperty(value) { jsElement[r'yetAnotherPublicProperty'] = (value is Map || (value is Iterable && value is! JsArray)) ? new JsObject.jsify(value) : value;} 21 | } 22 | -------------------------------------------------------------------------------- /e2e_test/test/expected/example_element_with_deep_behavior.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 2 | 3 | /// Dart API for the polymer element `example_element_with_deep_behavior`. 4 | @HtmlImport('example_element_with_deep_behavior_nodart.html') 5 | library e2e_test.lib.src.example_element_with_deep_behavior.example_element_with_deep_behavior; 6 | 7 | import 'dart:html'; 8 | import 'dart:js' show JsArray, JsObject; 9 | import 'package:web_components/web_components.dart'; 10 | import 'package:polymer_interop/polymer_interop.dart'; 11 | import 'example_multi_deep_behavior.dart'; 12 | import 'example_multi_behavior.dart'; 13 | import 'example_behavior.dart'; 14 | 15 | /// An example element with a behavior with deep dependencies. 16 | @CustomElementProxy('example-element-with-deep-behavior') 17 | class ExampleElementWithDeepBehavior extends HtmlElement with CustomElementProxyMixin, PolymerBase, ExampleBehavior, ExampleMultiBehavior, ExampleMultiDeepBehavior { 18 | ExampleElementWithDeepBehavior.created() : super.created(); 19 | factory ExampleElementWithDeepBehavior() => new Element.tag('example-element-with-deep-behavior'); 20 | } 21 | -------------------------------------------------------------------------------- /e2e_test/test/expected/example_multi_behavior.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 2 | 3 | /// Dart API for the polymer element `example_multi_behavior`. 4 | @HtmlImport('example_multi_behavior_nodart.html') 5 | library e2e_test.lib.src.example_multi_behavior.example_multi_behavior; 6 | 7 | import 'dart:html'; 8 | import 'dart:js' show JsArray, JsObject; 9 | import 'package:web_components/web_components.dart'; 10 | import 'package:polymer_interop/polymer_interop.dart'; 11 | import 'example_behavior.dart'; 12 | 13 | /// This is an example behavior! 14 | @BehaviorProxy(const ['Polymer', 'ExampleMultiBehavior']) 15 | abstract class ExampleMultiBehavior implements CustomElementProxyMixin, ExampleBehavior { 16 | 17 | /// A public property created with the properties descriptor. 18 | get anotherPublicProperty => jsElement[r'anotherPublicProperty']; 19 | set anotherPublicProperty(value) { jsElement[r'anotherPublicProperty'] = (value is Map || (value is Iterable && value is! JsArray)) ? new JsObject.jsify(value) : value;} 20 | 21 | /// [stringParam]: {string} 22 | String anotherBehaviorFunction(stringParam) => 23 | jsElement.callMethod('anotherBehaviorFunction', [stringParam]); 24 | } 25 | -------------------------------------------------------------------------------- /e2e_test/lib/src/example_element_with_behavior/example_element_with_behavior.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 25 | 26 | 39 | 40 | -------------------------------------------------------------------------------- /e2e_test/lib/src/example_element_with_deep_behavior/example_element_with_deep_behavior.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 14 | 15 | 21 | 22 | 23 | 26 | 27 | 40 | 41 | -------------------------------------------------------------------------------- /e2e_test/test/e2e_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('vm') 2 | library custom_element_apigen.test.behavior_test; 3 | 4 | import 'dart:io'; 5 | import 'package:custom_element_apigen/generate_dart_api.dart'; 6 | import 'package:test/test.dart'; 7 | import 'mock_file.dart'; 8 | 9 | _mockFileFactory(String path) => new MockFile(path); 10 | 11 | main() { 12 | test('can generate wrappers', () async { 13 | var config = parseArgs(['behavior_config.yaml'], ''); 14 | await generateWrappers(config, createFile: _mockFileFactory); 15 | expectFilesCreated('example_behavior'); 16 | expectFilesCreated('example_element'); 17 | expectFilesCreated('example_element_with_behavior'); 18 | expectFilesCreated('example_multi_behavior'); 19 | expectFilesCreated('example_multi_deep_behavior'); 20 | expectFilesCreated('example_element_with_deep_behavior'); 21 | }); 22 | } 23 | 24 | void expectFilesCreated(String name) { 25 | void expectContainsFile(String path) { 26 | expect(MockFile.createdFiles.any((f) => f.path == path), isTrue); 27 | } 28 | 29 | expectContainsFile('lib/$name.html'); 30 | expectContainsFile('lib/${name}_nodart.html'); 31 | expectContainsFile('lib/$name.dart'); 32 | expect(MockFile.createdFiles 33 | .firstWhere((f) => f.path == 'lib/$name.dart').contents, 34 | readExpected(name)); 35 | } 36 | 37 | String readExpected(String name) => 38 | new File('test/expected/$name.dart').readAsStringSync(); 39 | -------------------------------------------------------------------------------- /e2e_test/lib/src/example_multi_deep_behavior/example_multi_deep_behavior.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, the Dart project authors. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /e2e_test/lib/src/example_multi_behavior/example_multi_behavior.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 51 | -------------------------------------------------------------------------------- /e2e_test/test/expected/example_behavior.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 2 | 3 | /// Dart API for the polymer element `example_behavior`. 4 | @HtmlImport('example_behavior_nodart.html') 5 | library e2e_test.lib.src.example_behavior.example_behavior; 6 | 7 | import 'dart:html'; 8 | import 'dart:js' show JsArray, JsObject; 9 | import 'package:web_components/web_components.dart'; 10 | import 'package:polymer_interop/polymer_interop.dart'; 11 | 12 | /// This is an example behavior! 13 | @BehaviorProxy(const ['Polymer', 'ExampleBehavior']) 14 | abstract class ExampleBehavior implements CustomElementProxyMixin { 15 | 16 | num get behaviorNum => jsElement[r'behaviorNum']; 17 | set behaviorNum(num value) { jsElement[r'behaviorNum'] = value; } 18 | 19 | num get behaviorNumGetterOnly => jsElement[r'behaviorNumGetterOnly']; 20 | 21 | set behaviorNumSetterOnly(value) { jsElement[r'behaviorNumSetterOnly'] = (value is Map || (value is Iterable && value is! JsArray)) ? new JsObject.jsify(value) : value;} 22 | 23 | /// A public property created with the properties descriptor. 24 | get behaviorPublicProperty => jsElement[r'behaviorPublicProperty']; 25 | set behaviorPublicProperty(value) { jsElement[r'behaviorPublicProperty'] = (value is Map || (value is Iterable && value is! JsArray)) ? new JsObject.jsify(value) : value;} 26 | 27 | /// A read only property. 28 | num get behaviorReadOnlyProperty => jsElement[r'behaviorReadOnlyProperty']; 29 | set behaviorReadOnlyProperty(num value) { jsElement[r'behaviorReadOnlyProperty'] = value; } 30 | 31 | /// A property whose type will be overridden 32 | num get behaviorWrongTypeProperty => jsElement[r'behaviorWrongTypeProperty']; 33 | set behaviorWrongTypeProperty(num value) { jsElement[r'behaviorWrongTypeProperty'] = value; } 34 | 35 | /// [stringParam]: {string} 36 | String behaviorFunction(stringParam) => 37 | jsElement.callMethod('behaviorFunction', [stringParam]); 38 | 39 | behaviorVoidFunction() => 40 | jsElement.callMethod('behaviorVoidFunction', []); 41 | } 42 | -------------------------------------------------------------------------------- /e2e_test/test/expected/example_element.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 2 | 3 | /// Dart API for the polymer element `example_element`. 4 | @HtmlImport('example_element_nodart.html') 5 | library e2e_test.lib.src.example_element.example_element; 6 | 7 | import 'dart:html'; 8 | import 'dart:js' show JsArray, JsObject; 9 | import 'package:web_components/web_components.dart'; 10 | import 'package:polymer_interop/polymer_interop.dart'; 11 | 12 | /// An example element. 13 | @CustomElementProxy('example-element') 14 | class ExampleElement extends HtmlElement with CustomElementProxyMixin, PolymerBase { 15 | ExampleElement.created() : super.created(); 16 | factory ExampleElement() => new Element.tag('example-element'); 17 | 18 | /// A public Array property. 19 | List get elementArrayProperty => jsElement[r'elementArrayProperty']; 20 | set elementArrayProperty(List value) { jsElement[r'elementArrayProperty'] = (value != null && value is! JsArray) ? new JsObject.jsify(value) : value;} 21 | 22 | num get elementNum => jsElement[r'elementNum']; 23 | set elementNum(num value) { jsElement[r'elementNum'] = value; } 24 | 25 | num get elementNumGetterOnly => jsElement[r'elementNumGetterOnly']; 26 | 27 | set elementNumSetterOnly(num value) { jsElement[r'elementNumSetterOnly'] = value; } 28 | 29 | /// A public property created with the properties descriptor. 30 | get elementPublicProperty => jsElement[r'elementPublicProperty']; 31 | set elementPublicProperty(value) { jsElement[r'elementPublicProperty'] = (value is Map || (value is Iterable && value is! JsArray)) ? new JsObject.jsify(value) : value;} 32 | 33 | /// A read only property. 34 | num get elementReadOnlyProperty => jsElement[r'elementReadOnlyProperty']; 35 | set elementReadOnlyProperty(num value) { jsElement[r'elementReadOnlyProperty'] = value; } 36 | 37 | /// A property whose type will be overridden 38 | num get elementWrongTypeProperty => jsElement[r'elementWrongTypeProperty']; 39 | set elementWrongTypeProperty(num value) { jsElement[r'elementWrongTypeProperty'] = value; } 40 | 41 | String elementFunction(String stringParam) => 42 | jsElement.callMethod('elementFunction', [stringParam]); 43 | 44 | elementVoidFunction() => 45 | jsElement.callMethod('elementVoidFunction', []); 46 | } 47 | -------------------------------------------------------------------------------- /bin/update.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 3 | // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 4 | // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 5 | // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 6 | // Code distributed by Google as part of the polymer project is also 7 | // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 8 | 9 | import 'dart:io'; 10 | import 'package:path/path.dart' as path; 11 | import 'package:custom_element_apigen/generate_dart_api.dart' as generator; 12 | 13 | main(args) async { 14 | generator.GlobalConfig config = 15 | generator.parseArgs(args, 'pub run custom_elements_apigen:update'); 16 | 17 | // TODO(sigmund): find out if we can use a bower override for this. 18 | var file = new File(path.join('lib', 'src', 'polymer', 'polymer.html')); 19 | if (!file.existsSync()) { 20 | print('error: lib/src/polymer/polymer.html not found. This tool ' 21 | 'requires that you first run `bower install`, and configure it ' 22 | 'to place all sources under `lib/src/`. See README for details.'); 23 | exit(1); 24 | } 25 | 26 | await generator.generateWrappers(config); 27 | 28 | // The file may be deleted at some point during the generator, make sure it 29 | // still exists. 30 | file.createSync(recursive: true); 31 | file.writeAsStringSync(_POLYMER_HTML_FORWARD); 32 | } 33 | 34 | const String _POLYMER_HTML_FORWARD = ''' 35 | 43 | 44 | 45 | 46 | 47 | ${generator.EMPTY_SCRIPT_WORKAROUND_ISSUE_11} 48 | '''; 49 | -------------------------------------------------------------------------------- /e2e_test/lib/src/example_behavior/example_behavior.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 94 | -------------------------------------------------------------------------------- /e2e_test/lib/src/example_element/example_element.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 24 | 25 | 111 | 112 | -------------------------------------------------------------------------------- /lib/src/common.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 2 | // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 3 | // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 4 | // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 5 | // Code distributed by Google as part of the polymer project is also 6 | // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 7 | 8 | /// Common logic used by the code generated with `tool/generate_dart_api.dart`. 9 | library custom_element_apigen.src.common; 10 | 11 | import 'dart:html' show Element, DocumentFragment; 12 | import 'dart:js' as js; 13 | 14 | /// A simple mixin to make it easier to interoperate with the Javascript API of 15 | /// a browser object. This is mainly used by classes that expose a Dart API for 16 | /// Javascript custom elements. 17 | // TODO(sigmund): move this to polymer 18 | class DomProxyMixin { 19 | js.JsObject _proxy; 20 | js.JsObject get jsElement { 21 | if (_proxy == null) { 22 | _proxy = new js.JsObject.fromBrowserObject(this); 23 | } 24 | return _proxy; 25 | } 26 | } 27 | 28 | /// A mixin to make it easier to interoperate with Polymer JS elements. This 29 | /// exposes only a subset of the public api that is most useful from external 30 | /// elements. 31 | abstract class PolymerProxyMixin implements DomProxyMixin { 32 | /// The underlying Js Element's `$` property. 33 | js.JsObject get $ => jsElement[r'$']; 34 | 35 | /// By default the data bindings will be cleaned up when this custom element 36 | /// is detached from the document. Overriding this to return `true` will 37 | /// prevent that from happening. 38 | bool get preventDispose => jsElement['preventDispose']; 39 | set preventDispose(bool newValue) => jsElement['preventDispose'] = newValue; 40 | 41 | /// Force any pending property changes to synchronously deliver to handlers 42 | /// specified in the `observe` object. Note, normally changes are processed at 43 | /// microtask time. 44 | /// 45 | // Dart note: renamed to `deliverPropertyChanges` to be more consistent with 46 | // other polymer.dart elements. 47 | void deliverPropertyChanges() { 48 | jsElement.callMethod('deliverChanges', []); 49 | } 50 | 51 | /// Inject HTML which contains markup bound to this element into a target 52 | /// element (replacing target element content). 53 | DocumentFragment injectBoundHTML(String html, [Element element]) => 54 | jsElement.callMethod('injectBoundHTML', [html, element]); 55 | 56 | /// Creates dom cloned from the given template, instantiating bindings with 57 | /// this element as the template model and `PolymerExpressions` as the binding 58 | /// delegate. 59 | DocumentFragment instanceTemplate(Element template) => 60 | jsElement.callMethod('instanceTemplate', [template]); 61 | 62 | /// This method should rarely be used and only if `cancelUnbindAll` has been 63 | /// called to prevent element unbinding. In this case, the element's bindings 64 | /// will not be automatically cleaned up and it cannot be garbage collected by 65 | /// by the system. If memory pressure is a concern or a large amount of 66 | /// elements need to be managed in this way, `unbindAll` can be called to 67 | /// deactivate the element's bindings and allow its memory to be reclaimed. 68 | void unbindAll() => jsElement.callMethod('unbindAll', []); 69 | 70 | /// Call in `detached` to prevent the element from unbinding when it is 71 | /// detached from the dom. The element is unbound as a cleanup step that 72 | /// allows its memory to be reclaimed. If `cancelUnbindAll` is used, consider 73 | ///calling `unbindAll` when the element is no longer needed. This will allow 74 | ///its memory to be reclaimed. 75 | void cancelUnbindAll() => jsElement.callMethod('cancelUnbindAll', []); 76 | } -------------------------------------------------------------------------------- /lib/src/html_element_names.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | library custom_element_apigen.src.html_element_names; 5 | 6 | /** 7 | * HTML element to DOM type mapping. Source: 8 | * 9 | * 10 | * The 'HTML' prefix has been removed to match `dart:html`, as per: 11 | * 12 | * It does not appear any element types are being renamed other than the prefix. 13 | * However there does not appear to be the last subtypes for the following tags: 14 | * command, data, td, th, and time. 15 | */ 16 | const HTML_ELEMENT_NAMES = const { 17 | 'a': 'AnchorElement', 18 | 'abbr': 'Element', 19 | 'address': 'Element', 20 | 'area': 'AreaElement', 21 | 'article': 'Element', 22 | 'aside': 'Element', 23 | 'audio': 'AudioElement', 24 | 'b': 'Element', 25 | 'base': 'BaseElement', 26 | 'bdi': 'Element', 27 | 'bdo': 'Element', 28 | 'blockquote': 'QuoteElement', 29 | 'body': 'BodyElement', 30 | 'br': 'BRElement', 31 | 'button': 'ButtonElement', 32 | 'canvas': 'CanvasElement', 33 | 'caption': 'TableCaptionElement', 34 | 'cite': 'Element', 35 | 'code': 'Element', 36 | 'col': 'TableColElement', 37 | 'colgroup': 'TableColElement', 38 | 'command': 'Element', // see doc comment, was: 'CommandElement' 39 | 'data': 'Element', // see doc comment, was: 'DataElement' 40 | 'datalist': 'DataListElement', 41 | 'dd': 'Element', 42 | 'del': 'ModElement', 43 | 'details': 'DetailsElement', 44 | 'dfn': 'Element', 45 | 'dialog': 'DialogElement', 46 | 'div': 'DivElement', 47 | 'dl': 'DListElement', 48 | 'dt': 'Element', 49 | 'em': 'Element', 50 | 'embed': 'EmbedElement', 51 | 'fieldset': 'FieldSetElement', 52 | 'figcaption': 'Element', 53 | 'figure': 'Element', 54 | 'footer': 'Element', 55 | 'form': 'FormElement', 56 | 'h1': 'HeadingElement', 57 | 'h2': 'HeadingElement', 58 | 'h3': 'HeadingElement', 59 | 'h4': 'HeadingElement', 60 | 'h5': 'HeadingElement', 61 | 'h6': 'HeadingElement', 62 | 'head': 'HeadElement', 63 | 'header': 'Element', 64 | 'hgroup': 'Element', 65 | 'hr': 'HRElement', 66 | 'html': 'HtmlElement', 67 | 'i': 'Element', 68 | 'iframe': 'IFrameElement', 69 | 'img': 'ImageElement', 70 | 'input': 'InputElement', 71 | 'ins': 'ModElement', 72 | 'kbd': 'Element', 73 | 'keygen': 'KeygenElement', 74 | 'label': 'LabelElement', 75 | 'legend': 'LegendElement', 76 | 'li': 'LIElement', 77 | 'link': 'LinkElement', 78 | 'map': 'MapElement', 79 | 'mark': 'Element', 80 | 'menu': 'MenuElement', 81 | 'meta': 'MetaElement', 82 | 'meter': 'MeterElement', 83 | 'nav': 'Element', 84 | 'noscript': 'Element', 85 | 'object': 'ObjectElement', 86 | 'ol': 'OListElement', 87 | 'optgroup': 'OptGroupElement', 88 | 'option': 'OptionElement', 89 | 'output': 'OutputElement', 90 | 'p': 'ParagraphElement', 91 | 'param': 'ParamElement', 92 | 'pre': 'PreElement', 93 | 'progress': 'ProgressElement', 94 | 'q': 'QuoteElement', 95 | 'rp': 'Element', 96 | 'rt': 'Element', 97 | 'ruby': 'Element', 98 | 's': 'Element', 99 | 'samp': 'Element', 100 | 'script': 'ScriptElement', 101 | 'section': 'Element', 102 | 'select': 'SelectElement', 103 | 'small': 'Element', 104 | 'source': 'SourceElement', 105 | 'span': 'SpanElement', 106 | 'strong': 'Element', 107 | 'style': 'StyleElement', 108 | 'sub': 'Element', 109 | 'summary': 'Element', 110 | 'sup': 'Element', 111 | 'table': 'TableElement', 112 | 'tbody': 'TableSectionElement', 113 | 'td': 'TableCellElement', // see doc comment, was: 'TableDataCellElement' 114 | 'template': 'TemplateElement', 115 | 'textarea': 'TextAreaElement', 116 | 'tfoot': 'TableSectionElement', 117 | 'th': 'TableCellElement', // see doc comment, was: 'TableHeaderCellElement' 118 | 'thead': 'TableSectionElement', 119 | 'time': 'Element', // see doc comment, was: 'TimeElement' 120 | 'title': 'TitleElement', 121 | 'tr': 'TableRowElement', 122 | 'track': 'TrackElement', 123 | 'u': 'Element', 124 | 'ul': 'UListElement', 125 | 'var': 'Element', 126 | 'video': 'VideoElement', 127 | 'wbr': 'Element', 128 | }; 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.2+1 2 | 3 | * Update for `path` API changes 4 | 5 | ## 0.2.2 6 | 7 | * Added a new option to add extra imports on the dart side. Usefull when 8 | the generated import is wrong and you need to omit it and replace with 9 | the right one: 10 | 11 | - some_file/some_file.html: 12 | extra_imports: 13 | - package:polymer_elements/iron_resizable_behavior.dart 14 | 15 | # 0.2.1+1 16 | * Make sure we handle duplicate behavior/element names that come back from the 17 | `hydrolysis` tool. This happens when there is an `Impl` class and a public 18 | class by the same name. 19 | 20 | ## 0.2.1 21 | * Added `type_overrides` option, which allows you to override types for any 22 | fields in a class. This may later be extended to allow you to override the 23 | return types and argument types of methods as well, if needed. This should 24 | be supplied as an option to a file, and should look like the following: 25 | 26 | - some_file/some_file.html: 27 | type_overrides: 28 | SomeClassInMyFile: 29 | somePropertyName: 30 | type: Number 31 | 32 | ## 0.2.0+2 33 | * Allow setting list/map properties to null. 34 | * Don't re-jsify a JsArray in setters. 35 | * Add support for deeply nested behaviors. 36 | 37 | ## 0.2.0+1 38 | * Remove `void` return types from all functions, and leave them as dynamic 39 | instead. It is too common for js methods to be marked as having no return 40 | type when they in fact do return something :(. 41 | 42 | ## 0.2.0 43 | * Update to polymer js 1.0 versions of polymer_interop and web_components 44 | packages. Not compatible with 0.5 elements, they should remain on 0.1.7. 45 | 46 | ## 0.1.7+1 47 | * Add back `common.dart` since old generated elements import it. 48 | This will be deleted in the next breaking release. 49 | 50 | ## 0.1.7 51 | * Use `CustomElementProxyMixin` from `web_components` instead of 52 | `DomProxyMixin`. 53 | * Moved `PolymerProxyMixin` to the `polymer_interop` package. 54 | * Point to `polymer.html` from the `polymer_interop` package instead of the 55 | `polymer` package. 56 | 57 | ## 0.1.6 58 | * Added `files_to_load` option to config files. Any files listed will be 59 | loaded but not generated. Any mixins which come from another package and are 60 | stubbed out will need to be loaded this way. 61 | 62 | ## 0.1.5 63 | * Add `Element` as dart type for `Element` js type. 64 | 65 | ## 0.1.5 66 | * Support `file_overrides` option for each html file listed in config files. 67 | This should be a map of file name prefixes to a list of class names. All 68 | classes listed will be output to the corresponding file instead of the 69 | default one. 70 | 71 | ## 0.1.4+3 72 | * Make the parser a bit more lenient around parsing mixins. The name is only 73 | parsed up to the first space, which allows for comments or other things 74 | to follow the name. 75 | 76 | ## 0.1.4+2 77 | * Switch from `html5lib` to `html` package dependency. 78 | 79 | ## 0.1.4+1 80 | * Update `web_components` constraint. 81 | 82 | ## 0.1.4 83 | * Start using @HtmlImport and @CustomElementProxy. This should have no effect 84 | on existing applications, other than enabling them to remove some of their 85 | html imports if desired (a dart import alone is now sufficient). 86 | 87 | ## 0.1.3 88 | * Add support for various methods and properties from the Polymer base class. 89 | * Add support for mixins. 90 | 91 | ## 0.1.2+1 92 | * Increase upper bound on web_components to `<0.11.0`. 93 | 94 | ## 0.1.2 95 | * Add support for the `$` property. 96 | 97 | ## 0.1.1 98 | * Automatically include `packages/web_components/interop_support.html`. 99 | 100 | ## 0.1.0 101 | 102 | * **Breaking Change** Removed main() from `generate_dart_api`, 103 | `pub run custom_elements_apigen:update ...` is now the only way you should 104 | generate wrappers. 105 | * **Breaking Change** `deletion_patterns` option will now delete folders that 106 | match the supplied patterns as well as files. 107 | * **Breaking Change** Many functions in `generate_dart_api` were moved to be 108 | private. 109 | 110 | ## 0.0.3 111 | 112 | Added deletion_patterns option to the config. This is a list of regex patterns 113 | that match files under the lib/src directory. All matched files will be deleted, 114 | and directories are skipped. This happens before the stubs are generated so they 115 | will not be deleted if you list a folder containing a stub. 116 | 117 | ## 0.0.2+1 118 | 119 | Updated polymer dependency. 120 | 121 | ## 0.0.2 122 | 123 | Elements can now be built from code using a normal factory constructor, such as 124 | `new FooElement()`. It is still necessary however to include the html import for 125 | each element you wish to create this way. 126 | 127 | ## 0.0.1 128 | 129 | Initial version 130 | -------------------------------------------------------------------------------- /lib/src/js/analyze.js: -------------------------------------------------------------------------------- 1 | var hyd = require('hydrolysis'); 2 | 3 | var filePath = process.argv[2]; 4 | 5 | 6 | try { 7 | hyd.Analyzer.analyze(filePath) 8 | .then(function (results) { 9 | console.log(JSON.stringify({ 10 | imports: results.html[filePath].depHrefs, 11 | elements: getElements(results, filePath), 12 | behaviors: getBehaviors(results, filePath), 13 | path: filePath 14 | })); 15 | }); 16 | } catch(e) { 17 | console.log(e); 18 | } 19 | function getElements(results, filePath) { 20 | var elements = {}; 21 | if (!results.elements) return elements; 22 | for (var e = 0; e < results.elements.length; e++) { 23 | var element = results.elements[e]; 24 | if (element.contentHref != filePath) continue; 25 | var className = toCamelCase(element.is); 26 | if (elements[className]) continue; 27 | 28 | elements[className] = { 29 | extendsName: getExtendsName(element), 30 | name: element.is, 31 | properties: getProperties(element), 32 | methods: getMethods(element), 33 | description: element.desc, 34 | behaviors: element.behaviors || [] 35 | }; 36 | } 37 | return elements; 38 | } 39 | 40 | function getBehaviors(results, filePath) { 41 | var behaviors = {}; 42 | if (!results.behaviors) return behaviors; 43 | for (var b = 0; b < results.behaviors.length; b++) { 44 | var behavior = results.behaviors[b]; 45 | if (behavior.contentHref != filePath) continue; 46 | var name = behavior.is.replace('Polymer.', ''); 47 | if (behaviors[name]) continue; 48 | 49 | behaviors[name] = { 50 | name: name, 51 | properties: getProperties(behavior), 52 | methods: getMethods(behavior), 53 | description: behavior.desc, 54 | behaviors: behavior.behaviors 55 | }; 56 | } 57 | return behaviors; 58 | } 59 | 60 | function getProperties(element) { 61 | var properties = {}; 62 | if (!element.properties) return properties; 63 | for (var i = 0; i < element.properties.length; i++) { 64 | var property = element.properties[i]; 65 | if (isPrivate(property) || !isField(property)) continue; 66 | if (property.name == 'extends') continue; 67 | if (properties[property.name]) continue; 68 | 69 | properties[property.name] = { 70 | hasGetter: !property.function || isGetter(property) || 71 | (isSetter(property) && hasPropertyGetter(element, property.name)), 72 | hasSetter: !property.function || isSetter(property) || 73 | (isGetter(property) && hasPropertySetter(element, property.name)), 74 | name: property.name, 75 | type: getFieldType(property), 76 | description: property.desc || '' 77 | }; 78 | } 79 | return properties; 80 | } 81 | 82 | function getFieldType(property) { 83 | if (isGetter(property)) return property.return ? property.return.type : null; 84 | if (isSetter(property)) return property.params[0].type; 85 | return property.type; 86 | } 87 | 88 | function getMethods(element) { 89 | var methods = {}; 90 | if (!element.properties) return methods; 91 | for (var i = 0; i < element.properties.length; i++) { 92 | var property = element.properties[i]; 93 | if (isPrivate(property) || !isMethod(property)) continue; 94 | if (methods[property.name]) continue; 95 | 96 | methods[property.name] = { 97 | name: property.name, 98 | type: property.return ? property.return.type : null, 99 | description: property.desc || '', 100 | isVoid: !property.return, 101 | args: getArguments(property) 102 | }; 103 | } 104 | return methods; 105 | } 106 | 107 | function getArguments(func) { 108 | var args = []; 109 | if (!func.params) return args; 110 | for (var i = 0; i < func.params.length; i++) { 111 | var param = func.params[i]; 112 | args.push({ 113 | name: param.name, 114 | description: param.desc || '', 115 | type: param.type 116 | }); 117 | } 118 | return args; 119 | } 120 | 121 | function getExtendsName(element) { 122 | if (!element.properties) return null; 123 | for (var i = 0; i < element.properties.length; i++) { 124 | var prop = element.properties[i]; 125 | if (prop.name == 'extends') { 126 | return prop.javascriptNode.value.value; 127 | } 128 | } 129 | } 130 | 131 | function isPrivate(property) { 132 | return property.name.length > 0 && property.name[0] == '_'; 133 | } 134 | 135 | function isMethod(property) { 136 | if (!property.function) return false; 137 | return !isGetter(property) && !isSetter(property); 138 | } 139 | 140 | function isField(property) { 141 | if (!property.function) return true; 142 | return isGetter(property) || isSetter(property); 143 | } 144 | 145 | function isGetter(field) { 146 | if (!field.function) return false; 147 | return field.javascriptNode.kind == 'get'; 148 | } 149 | 150 | function isSetter(field) { 151 | if (!field.function) return false; 152 | return field.javascriptNode.kind == 'set'; 153 | } 154 | 155 | function hasPropertySetter(element, name) { 156 | for (var i = 0; i < element.properties.length; i++) { 157 | var prop = element.properties[i]; 158 | if (prop.name == name && prop.function && prop.javascriptNode.kind == 'set') 159 | return true; 160 | } 161 | return false; 162 | } 163 | 164 | function hasPropertyGetter(element, name) { 165 | for (var i = 0; i < element.properties.length; i++) { 166 | var prop = element.properties[i]; 167 | if (prop.name == name && prop.function && prop.javascriptNode.kind == 'get') 168 | return true; 169 | } 170 | return false; 171 | } 172 | 173 | function toCamelCase(dashName) { 174 | return dashName.split('-').map(function (e) { 175 | return e.substring(0, 1).toUpperCase() + e.substring(1); 176 | }).join(''); 177 | } 178 | -------------------------------------------------------------------------------- /lib/src/config.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 2 | // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 3 | // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 4 | // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 5 | // Code distributed by Google as part of the polymer project is also 6 | // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 7 | 8 | /// Helper to parse code generation configurations from a file. 9 | library custom_element_apigen.src.config; 10 | 11 | import 'dart:io'; 12 | import 'package:path/path.dart' as path; 13 | import 'package:yaml/yaml.dart'; 14 | 15 | /// Holds the entire information parsed from the command line arguments and 16 | /// configuration files. 17 | class GlobalConfig { 18 | final List files = []; 19 | final Map stubs = {}; 20 | final List packageMappings = []; 21 | final List deletionPatterns = []; 22 | final List filesToLoad = []; 23 | String currentPackage; 24 | int _lastUpdated = 0; 25 | 26 | /// Retrieve the package name associated with [elementName]. 27 | String findPackageNameForElement(String elementName) { 28 | if (_lastUpdated != packageMappings.length) { 29 | packageMappings.sort(); 30 | } 31 | for (var mapping in packageMappings) { 32 | if (mapping._regex.hasMatch(elementName)) return mapping.packageName; 33 | } 34 | return null; 35 | } 36 | } 37 | 38 | class PackageMapping implements Comparable { 39 | final String pattern; 40 | final String packageName; 41 | final RegExp _regex; 42 | 43 | PackageMapping(String pattern, this.packageName) 44 | : pattern = pattern, 45 | _regex = new RegExp(pattern); 46 | 47 | /// Sort in reverse order of the prefix to ensure that longer prefixes are 48 | /// matched first. 49 | int compareTo(PackageMapping other) => -pattern.compareTo(other.pattern); 50 | } 51 | 52 | /// Configuration information corresponding to a given HTML input file. 53 | class FileConfig { 54 | final GlobalConfig global; 55 | 56 | /// The path to the original file. 57 | final String inputPath; 58 | 59 | /// Javascript names that should be substituted when generating Dart code. 60 | final Map nameSubstitutions; 61 | 62 | /// HTML Imports that shold not be mirrored because they don't have a 63 | /// corresponding Dart type. 64 | final List omitImports; 65 | 66 | /// extra imports 67 | final List extraImports; 68 | 69 | /// Map of file names to classes that should live within them. All other 70 | /// classes will end up in the default file. 71 | final Map> file_overrides; 72 | 73 | /// Map of type overrides for classes. Should be in this form: 74 | /// 75 | /// - example_element/example_element.html: 76 | /// type_overrides: 77 | /// ExampleElement: 78 | /// exampleProperty: 79 | /// type: Number 80 | /// 81 | /// These are often needed when js types are wrong. 82 | final Map>>> typeOverrides; 84 | 85 | /// Dart import to get the base class of a custom element. This is inferred 86 | /// normally from the package_mappings, but can be overriden on an individual 87 | /// file if necessary. 88 | final String extendsImport; 89 | 90 | FileConfig(this.global, this.inputPath, [Map map]) 91 | : nameSubstitutions = map != null ? map['name_substitutions'] : null, 92 | omitImports = map != null ? map['omit_imports'] : null, 93 | extraImports = map != null ? map['extra_imports'] : null, 94 | extendsImport = map != null ? map['extends_import'] : null, 95 | file_overrides = map != null ? map['file_overrides'] : null, 96 | typeOverrides = map != null ? map['type_overrides'] : null; 97 | } 98 | 99 | /// Parse configurations from a `.yaml` configuration file. 100 | void parseConfigFile(String filePath, GlobalConfig config) { 101 | if (!new File(filePath).existsSync()) { 102 | print("error: file $filePath doesn't exist"); 103 | exit(1); 104 | } 105 | var yaml = loadYaml(new File(filePath).readAsStringSync()); 106 | _parsePackageMappings(yaml, config); 107 | _parseFilesToGenerate(yaml, config); 108 | _parseStubsToGenerate(yaml, config); 109 | _parseDeletionPatterns(yaml, config); 110 | _parseFilesToLoad(yaml, config); 111 | 112 | if (!new File('pubspec.yaml').existsSync()) { 113 | print("error: file 'pubspec.yaml' doesn't exist"); 114 | exit(1); 115 | } 116 | yaml = loadYaml(new File('pubspec.yaml').readAsStringSync()); 117 | config.currentPackage = yaml['name']; 118 | } 119 | 120 | void _parsePackageMappings(yaml, GlobalConfig config) { 121 | var packageMappings = yaml['package_mappings']; 122 | if (packageMappings == null) return; 123 | for (var entry in packageMappings) { 124 | if (entry is! YamlMap) continue; 125 | config.packageMappings 126 | .add(new PackageMapping(entry.keys.single, entry.values.single)); 127 | } 128 | } 129 | 130 | void _parseFilesToGenerate(yaml, GlobalConfig config) { 131 | var toGenerate = yaml['files_to_generate']; 132 | if (toGenerate == null) return; 133 | for (var entry in toGenerate) { 134 | if (entry is String) { 135 | config.files.add(new FileConfig(config, path.join('lib', 'src', entry))); 136 | continue; 137 | } 138 | 139 | if (entry is! YamlMap) continue; 140 | if (entry.length != 1) { 141 | print('invalid format for: $entry'); 142 | continue; 143 | } 144 | 145 | config.files.add(new FileConfig(config, 146 | path.join('lib', 'src', entry.keys.single), entry.values.single)); 147 | } 148 | } 149 | 150 | void _parseStubsToGenerate(yaml, GlobalConfig config) { 151 | var toGenerate = yaml['stubs_to_generate']; 152 | if (toGenerate == null) return; 153 | if (toGenerate is! YamlMap) { 154 | print("error: bad configuration in stubs_to_generate"); 155 | exit(1); 156 | } 157 | var map = toGenerate as YamlMap; 158 | for (var key in map.keys) { 159 | var value = map[key]; 160 | if (value is String) { 161 | config.stubs[path.join('lib', 'src', value)] = key; 162 | continue; 163 | } 164 | if (value is YamlList) { 165 | for (var entry in value) { 166 | config.stubs[path.join('lib', 'src', entry)] = key; 167 | } 168 | } 169 | } 170 | } 171 | 172 | void _parseDeletionPatterns(yaml, GlobalConfig config) { 173 | var patterns = _parseStringList(yaml, 'deletion_patterns'); 174 | if (patterns == null) return; 175 | config.deletionPatterns 176 | .addAll((patterns as YamlList).map((pattern) => new RegExp(pattern))); 177 | } 178 | 179 | void _parseFilesToLoad(yaml, GlobalConfig config) { 180 | var filePaths = _parseStringList(yaml, 'files_to_load'); 181 | if (filePaths == null) return; 182 | config.filesToLoad.addAll(filePaths.map((filePath) { 183 | var parts = filePath.split(':'); 184 | if (parts.length == 1) { 185 | return platformIndependentPath(parts[0]); 186 | } else if (parts.length == 2 && parts[0] == 'package') { 187 | return path.join('packages', platformIndependentPath(parts[1])); 188 | } else { 189 | throw 'Unrecognized path `$filePath`. Should be a relative uri or a ' 190 | '`package:` style uri.'; 191 | } 192 | })); 193 | } 194 | 195 | String platformIndependentPath(String originalPath) => 196 | path.joinAll(path.url.split(originalPath)); 197 | 198 | List _parseStringList(yaml, String name) { 199 | var items = yaml[name]; 200 | if (items == null) return null; 201 | if (items is! YamlList || (items as YamlList).any((e) => e is! String)) { 202 | print('Unrecognized $name setting, expected a list of Strings'); 203 | exit(1); 204 | } 205 | return items; 206 | } 207 | -------------------------------------------------------------------------------- /lib/src/ast.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 2 | // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 3 | // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 4 | // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 5 | // Code distributed by Google as part of the polymer project is also 6 | // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 7 | 8 | /// AST nodes to represent the API of a Javascript polymer custom element. This 9 | /// is parsed from documentation found in polymer elements and then it is used 10 | /// to autogenerate a Dart API for them. 11 | library custom_element_apigen.src.ast; 12 | 13 | import 'codegen.dart' show toCamelCase; 14 | 15 | class FileSummary { 16 | String path; 17 | List imports = []; 18 | Map elementsMap = {}; 19 | Map mixinsMap = {}; 20 | 21 | FileSummary(); 22 | 23 | FileSummary.fromJson(Map jsonSummary) { 24 | imports = jsonSummary['imports'].map((path) => new Import(path)).toList(); 25 | 26 | for (Map element in jsonSummary['elements'].values) { 27 | elementsMap[element['name']] = new Element.fromJson(element); 28 | } 29 | 30 | for (Map mixinMap in jsonSummary['behaviors'].values) { 31 | var mixin = new Mixin.fromJson(mixinMap); 32 | mixinsMap[mixin.name] = mixin; 33 | } 34 | 35 | path = jsonSummary['path']; 36 | } 37 | 38 | Iterable get elements => elementsMap.values; 39 | Iterable get mixins => mixinsMap.values; 40 | 41 | String toString() => 42 | 'imports:\n$imports, elements:\n$elements, mixins:\n$mixins'; 43 | 44 | /// Splits this summary into multiple summaries based on [file_overrides]. The 45 | /// keys are file names and the values are classes that should live in that 46 | /// file. All remaining files will end up in the [null] key. 47 | Map splitByFile( 48 | Map> file_overrides) { 49 | if (file_overrides == null) return {null: this}; 50 | 51 | var summaries = {}; 52 | var remainingElements = new Map.from(elementsMap); 53 | var remainingMixins = new Map.from(mixinsMap); 54 | 55 | /// Removes [names] keys from [original] and returns a new [Map] with those 56 | /// removed values. 57 | Map removeFromMap( 58 | Map original, List names) { 59 | var newMap = {}; 60 | for (var name in names) { 61 | var val = original.remove(name); 62 | if (val != null) newMap[name] = val; 63 | } 64 | return newMap; 65 | } 66 | 67 | /// Builds a summary from this one given [classNames]. 68 | FileSummary buildSummary(List classNames) { 69 | return new FileSummary() 70 | ..imports = new List.from(imports) 71 | ..elementsMap = removeFromMap(remainingElements, classNames) 72 | ..mixinsMap = removeFromMap(remainingMixins, classNames); 73 | } 74 | 75 | file_overrides.forEach((String path, List classNames) { 76 | summaries[path] = buildSummary(classNames); 77 | }); 78 | 79 | var defaultSummary = new FileSummary() 80 | ..imports = new List.from(imports) 81 | ..elementsMap = remainingElements 82 | ..mixinsMap = remainingMixins; 83 | 84 | summaries[null] = defaultSummary; 85 | 86 | return summaries; 87 | } 88 | } 89 | 90 | /// Base class for any entry we parse out of the HTML files. 91 | abstract class Entry { 92 | String toString() { 93 | var sb = new StringBuffer(); 94 | _prettyPrint(sb); 95 | return sb.toString(); 96 | } 97 | 98 | void _prettyPrint(StringBuffer sb); 99 | } 100 | 101 | /// Common information to most entries (element, property, method, etc). 102 | abstract class NamedEntry { 103 | final String name; 104 | String description; 105 | FileSummary summary; 106 | 107 | NamedEntry.fromJson(Map jsonNamedEntry) 108 | : name = jsonNamedEntry['name'], 109 | description = jsonNamedEntry['description']; 110 | } 111 | 112 | /// An entry that has type information (like arguments and properties). 113 | abstract class TypedEntry extends NamedEntry { 114 | String type; 115 | 116 | TypedEntry.fromJson(Map jsonTypedEntry) 117 | : type = jsonTypedEntry['type'], 118 | super.fromJson(jsonTypedEntry); 119 | } 120 | 121 | /// An import to another html element. 122 | class Import extends Entry { 123 | String importPath; 124 | Import(this.importPath); 125 | 126 | void _prettyPrint(StringBuffer sb) { 127 | sb.write('import: $importPath\n'); 128 | } 129 | } 130 | 131 | class Class extends NamedEntry { 132 | // TODO(jakemac): Rename to `extendsName`. 133 | String extendName; 134 | final Map properties = {}; 135 | final List methods = []; 136 | 137 | Class.fromJson(Map jsonClass) : super.fromJson(jsonClass) { 138 | extendName = jsonClass['extendsName']; 139 | 140 | for (Map property in jsonClass['properties'].values) { 141 | properties[property['name']] = new Property.fromJson(property); 142 | } 143 | 144 | for (Map method in jsonClass['methods'].values) { 145 | methods.add(new Method.fromJson(method)); 146 | } 147 | } 148 | 149 | void _prettyPrint(StringBuffer sb) { 150 | sb.write('$name:\n'); 151 | sb.write('properties:\n'); 152 | for (var p in properties.values) { 153 | sb.write(' - '); 154 | p._prettyPrint(sb); 155 | sb.write('\n'); 156 | } 157 | sb.write('methods:\n'); 158 | for (var m in methods) { 159 | sb.write(' - '); 160 | m._prettyPrint(sb); 161 | sb.write('\n'); 162 | } 163 | sb.writeln('extends: $extendName'); 164 | } 165 | 166 | String toString() { 167 | var message = new StringBuffer(); 168 | _prettyPrint(message); 169 | return message.toString(); 170 | } 171 | } 172 | 173 | class Mixin extends Class { 174 | final List additionalMixins; 175 | 176 | Mixin.fromJson(Map jsonMixin) 177 | : additionalMixins = _allMixinNames(jsonMixin['behaviors']), 178 | super.fromJson(jsonMixin); 179 | 180 | _prettyPrint(StringBuffer sb) { 181 | sb.writeln('**Mixin**'); 182 | super._prettyPrint(sb); 183 | } 184 | 185 | String toString() { 186 | var message = new StringBuffer(); 187 | _prettyPrint(message); 188 | return message.toString(); 189 | } 190 | } 191 | 192 | /// Data about a custom-element. 193 | class Element extends Class { 194 | final List mixins; 195 | 196 | Element.fromJson(Map jsonElement) 197 | : mixins = _allMixinNames(jsonElement['behaviors']), 198 | super.fromJson(jsonElement); 199 | 200 | void _prettyPrint(StringBuffer sb) { 201 | sb.writeln('**Element**'); 202 | super._prettyPrint(sb); 203 | sb.writeln(' mixins:\n'); 204 | for (var mixin in mixins) { 205 | sb.writeln(' - $mixin'); 206 | } 207 | sb.writeln(' extends: $extendName'); 208 | } 209 | 210 | String toString() { 211 | var message = new StringBuffer(); 212 | _prettyPrint(message); 213 | return message.toString(); 214 | } 215 | } 216 | 217 | /// Data about a property. 218 | class Property extends TypedEntry { 219 | bool hasGetter; 220 | bool hasSetter; 221 | 222 | Property.fromJson(Map jsonProperty) : super.fromJson(jsonProperty) { 223 | hasGetter = jsonProperty['hasGetter']; 224 | hasSetter = jsonProperty['hasSetter']; 225 | } 226 | 227 | void _prettyPrint(StringBuffer sb) { 228 | sb.write('$type $name;'); 229 | } 230 | } 231 | 232 | /// Data about a method. 233 | class Method extends TypedEntry { 234 | bool isVoid = true; 235 | List args = []; 236 | List optionalArgs = []; 237 | 238 | Method.fromJson(Map jsonMethod) : super.fromJson(jsonMethod) { 239 | isVoid = jsonMethod['isVoid']; 240 | 241 | for (Map arg in jsonMethod['args']) { 242 | args.add(new Argument.fromJson(arg)); 243 | } 244 | 245 | // TODO(jakemac): Support optional args. 246 | } 247 | 248 | void _prettyPrint(StringBuffer sb) { 249 | if (isVoid) sb.write('void '); 250 | sb.write('$name('); 251 | 252 | bool first = true; 253 | for (var arg in args) { 254 | if (!first) sb.write(','); 255 | first = false; 256 | arg._prettyPrint(sb); 257 | } 258 | 259 | bool firstOptional = true; 260 | for (var arg in optionalArgs) { 261 | if (firstOptional) { 262 | if (!first) sb.write(','); 263 | sb.write('['); 264 | } else { 265 | sb.write(','); 266 | } 267 | first = false; 268 | firstOptional = false; 269 | arg._prettyPrint(sb); 270 | } 271 | if (!firstOptional) sb.write(']'); 272 | 273 | sb.write(');'); 274 | } 275 | } 276 | 277 | /// Collects name and type information for arguments. 278 | class Argument extends TypedEntry { 279 | Argument.fromJson(Map jsonArgument) : super.fromJson(jsonArgument); 280 | 281 | void _prettyPrint(StringBuffer sb) { 282 | if (type != null) { 283 | sb 284 | ..write(type) 285 | ..write(' '); 286 | } 287 | sb.write(name); 288 | } 289 | } 290 | 291 | List _flatten(List items) { 292 | var flattened = []; 293 | 294 | addAll(items) { 295 | for (var item in items) { 296 | if (item is List) { 297 | addAll(item); 298 | } else { 299 | flattened.add(item); 300 | } 301 | } 302 | } 303 | addAll(items); 304 | 305 | return flattened; 306 | } 307 | 308 | List _allMixinNames(List behaviorNames) { 309 | if (behaviorNames == null) return null; 310 | var names = []; 311 | for (String mixin in _flatten(behaviorNames)) { 312 | names.add(mixin.replaceFirst('Polymer.', '')); 313 | } 314 | return names; 315 | } 316 | -------------------------------------------------------------------------------- /lib/generate_dart_api.dart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dart 2 | // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 3 | // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 4 | // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 5 | // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 6 | // Code distributed by Google as part of the polymer project is also 7 | // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 8 | 9 | import 'dart:async'; 10 | import 'dart:convert'; 11 | import 'dart:io'; 12 | import 'package:path/path.dart' as path; 13 | import 'src/ast.dart'; 14 | import 'src/codegen.dart'; 15 | import 'src/config.dart'; 16 | export 'src/config.dart' show GlobalConfig; 17 | 18 | bool verbose = false; 19 | 20 | // Allows a way to test things without actually writing to the file system. 21 | typedef File FileFactory(String path); 22 | File defaultCreateFile(String path) => new File(path); 23 | 24 | GlobalConfig parseArgs(args, String program) { 25 | if (args.length == 0) { 26 | print('usage: call this tool with either input files ' 27 | 'or a configuration file that describes input files and name ' 28 | 'substitutions. For example: '); 29 | print(' $program lib/src/x-a/x-a.html lib/src/x-b/x-c.html ...'); 30 | print(' $program config.yaml'); 31 | print(' $program config.yaml lib/src/x-a/x-a.html config2.yaml'); 32 | exit(1); 33 | } 34 | 35 | var config = new GlobalConfig(); 36 | for (var arg in args) { 37 | if (arg.endsWith('.html')) { 38 | config.files.add(new FileConfig(config, arg)); 39 | } else if (arg.endsWith('.yaml')) { 40 | _progress('Parsing configuration ... '); 41 | parseConfigFile(arg, config); 42 | } 43 | } 44 | 45 | return config; 46 | } 47 | 48 | Future generateWrappers(GlobalConfig config, 49 | {FileFactory createFile: defaultCreateFile}) async { 50 | var fileSummaries = []; 51 | var elementSummaries = {}; 52 | var mixinSummaries = {}; 53 | var len = config.files.length; 54 | int i = 0; 55 | 56 | // Parses a file at [path] into a [FileSummary] and adds everything found into 57 | // [fileSummaries], [elementSummaries], and [mixinSummaries]. 58 | Future parseFile(String path, int totalLength) async { 59 | _progress('${++i} of $totalLength: $path'); 60 | var summary = await _parseFile(path, config); 61 | fileSummaries.add(summary); 62 | for (var elementSummary in summary.elements) { 63 | elementSummary.summary = summary; 64 | var name = elementSummary.name; 65 | if (elementSummaries.containsKey(name)) { 66 | print('Error: found two elements with the same name ${name}'); 67 | exit(1); 68 | } 69 | elementSummaries[name] = elementSummary; 70 | } 71 | for (var mixinSummary in summary.mixins) { 72 | mixinSummary.summary = summary; 73 | var name = mixinSummary.name; 74 | if (mixinSummaries.containsKey(name)) { 75 | print('Error: found two mixins with the same name ${name}'); 76 | exit(1); 77 | } 78 | mixinSummaries[name] = mixinSummary; 79 | } 80 | } 81 | 82 | _progress('Parsing files... '); 83 | var parsedFilesLength = config.files.length + config.filesToLoad.length; 84 | await Future.forEach(config.files, (fileConfig) async { 85 | await parseFile(fileConfig.inputPath, parsedFilesLength); 86 | }); 87 | await Future.forEach(config.filesToLoad, 88 | (path) async => await parseFile(path, parsedFilesLength)); 89 | 90 | _progress('Running codegen... '); 91 | len = config.files.length; 92 | i = 0; 93 | config.files.forEach((fileConfig) { 94 | var inputPath = fileConfig.inputPath; 95 | var fileSummary = fileSummaries[i]; 96 | _progress('${++i} of $len: $inputPath'); 97 | 98 | var splitSummaries = fileSummary.splitByFile(fileConfig.file_overrides); 99 | splitSummaries.forEach((String filePath, FileSummary summary) { 100 | _generateDartApi(summary, elementSummaries, mixinSummaries, inputPath, 101 | fileConfig, filePath, createFile); 102 | }); 103 | }); 104 | 105 | // We assume that the file has to be there because of bower, even though we 106 | // could generate without. 107 | _progress('Checking original files exist for stubs'); 108 | for (var inputPath in config.stubs.keys) { 109 | var file = new File(inputPath); 110 | if (!file.existsSync()) { 111 | print("error: stub file $inputPath doesn't exist"); 112 | exit(1); 113 | } 114 | } 115 | 116 | _progress('Deleting files... '); 117 | _deleteFilesMatchingPatterns(config.deletionPatterns); 118 | 119 | _progress('Generating stubs... '); 120 | len = config.stubs.length; 121 | i = 0; 122 | config.stubs.forEach((inputPath, packageName) { 123 | _progress('${++i} of $len: $inputPath'); 124 | _generateImportStub(inputPath, packageName, createFile); 125 | }); 126 | 127 | _progress('Done'); 128 | stdout.write('\n'); 129 | } 130 | 131 | void _generateImportStub( 132 | String inputPath, String packageName, FileFactory createFile) { 133 | var file = createFile(inputPath); 134 | // File may have been deleted, make sure it still exists. 135 | file.createSync(recursive: true); 136 | 137 | var segments = path.split(inputPath); 138 | var newFileName = 139 | segments.last.replaceAll('-', '_').replaceAll('.html', '_nodart.html'); 140 | var depth = segments.length; 141 | var goingUp = '../' * depth; 142 | var newPath = path.join(goingUp, 'packages/$packageName', newFileName); 143 | file.writeAsStringSync('\n' 144 | '$EMPTY_SCRIPT_WORKAROUND_ISSUE_11'); 145 | } 146 | 147 | /// Reads the contents of [inputPath], parses the documentation, and then 148 | /// generates a FileSummary for it. 149 | Future _parseFile(String inputPath, GlobalConfig globalConfig, 150 | {bool ignoreFileErrors: false}) async { 151 | var results = await Process.run( 152 | 'packages/custom_element_apigen/src/js/process_elements.sh', [inputPath]); 153 | if (results.exitCode != 0 || results.stderr != '') _parseError(results); 154 | 155 | var jsonFileSummary; 156 | try { 157 | jsonFileSummary = JSON.decode(results.stdout); 158 | assert(jsonFileSummary is Map); 159 | } catch (e) { 160 | _parseError(results); 161 | } 162 | 163 | // Apply type_overrides early on! 164 | _applyTypeOverrides(inputPath, jsonFileSummary, globalConfig); 165 | 166 | return new FileSummary.fromJson(jsonFileSummary); 167 | } 168 | 169 | void _applyTypeOverrides( 170 | String inputPath, Map jsonFileSummary, GlobalConfig globalConfig) { 171 | // Get the file config matching inputPath, and bail if it has no overrides. 172 | var fileConfig = globalConfig.files.firstWhere( 173 | (file) => platformIndependentPath(file.inputPath) == inputPath, 174 | orElse: () => null); 175 | if (fileConfig == null || fileConfig.typeOverrides == null) return; 176 | 177 | // Apply each type override to the jsonFileSummary. 178 | fileConfig.typeOverrides.forEach((String className, Map details) { 179 | // Find the summary for the class, it could be either an element or a 180 | // behavior. 181 | var jsonClass = jsonFileSummary['elements'][className]; 182 | if (jsonClass == null) { 183 | jsonClass = jsonFileSummary['behaviors'][className]; 184 | } 185 | if (jsonClass == null) { 186 | throw 'Unable to find class $className in file at $inputPath'; 187 | } 188 | 189 | // Apply the type overrides for each named property. 190 | details.forEach((String propertyName, Map propertyDetail) { 191 | var jsonProperty = jsonClass['properties'][propertyName]; 192 | if (jsonProperty == null) { 193 | throw 'unable to find property $propertyName in class $className'; 194 | } 195 | 196 | jsonProperty['type'] = propertyDetail['type']; 197 | }); 198 | }); 199 | } 200 | 201 | _parseError(ProcessResult results) { 202 | throw ''' 203 | Failed to parse element files! 204 | 205 | exit code: ${results.exitCode} 206 | stderr: ${results.stderr} 207 | stdout: ${results.stdout} 208 | '''; 209 | } 210 | 211 | /// Takes a FileSummary, and generates a Dart API for it. The input code must be 212 | /// under lib/src/ (for example, lib/src/x-tag/x-tag.html), the output will be 213 | /// generated under lib/ (for example, lib/x_tag/x_tag.dart). 214 | /// 215 | /// If [fileName] is supplied then that will be used as the prefix for all 216 | /// output files. 217 | void _generateDartApi( 218 | FileSummary summary, 219 | Map elementSummaries, 220 | Map mixinSummaries, 221 | String inputPath, 222 | FileConfig config, 223 | String filePath, 224 | FileFactory createFile) { 225 | _progressLineBroken = false; 226 | var segments = path.split(inputPath); 227 | if (segments.length < 4 || 228 | segments[0] != 'lib' || 229 | segments[1] != 'src' || 230 | !segments.last.endsWith('.html')) { 231 | print('error: expected $inputPath to be of the form ' 232 | 'lib/src/x-tag/**/x-tag2.html'); 233 | exit(1); 234 | } 235 | 236 | var dashName = path.joinAll(segments.getRange(2, segments.length)); 237 | // Use the filename if overridden. 238 | var name = filePath != null 239 | ? filePath 240 | : path.withoutExtension(segments.last).replaceAll('-', '_'); 241 | var isSubdir = segments.length > 4; 242 | var outputDirSegments = ['lib']; 243 | if (isSubdir) { 244 | outputDirSegments.addAll(segments 245 | .getRange(2, segments.length - 1) 246 | .map((s) => s.replaceAll('-', '_'))); 247 | } 248 | var packageLibDir = (isSubdir) ? '../' * (segments.length - 3) : ''; 249 | var outputDir = path.joinAll(outputDirSegments); 250 | 251 | // Create the dart file. 252 | var dartContent = new StringBuffer(); 253 | dartContent.write(generateDirectives( 254 | name, segments, summary, config, packageLibDir, mixinSummaries)); 255 | var first = true; 256 | for (var element in summary.elements) { 257 | if (!first) dartContent.write('\n\n'); 258 | dartContent.write( 259 | generateClass(element, config, elementSummaries, mixinSummaries)); 260 | first = false; 261 | } 262 | for (var mixin in summary.mixins) { 263 | if (!first) dartContent.write('\n\n'); 264 | dartContent 265 | .write(generateClass(mixin, config, elementSummaries, mixinSummaries)); 266 | first = false; 267 | } 268 | createFile(path.join(outputDir, '$name.dart')) 269 | ..createSync(recursive: true) 270 | ..writeAsStringSync(dartContent.toString()); 271 | 272 | // Create the main html file, this contains an import to the *_nodart.html 273 | // file, as well as other imports and a script pointing to the dart file. 274 | var extraImports = new StringBuffer(); 275 | for (var jsImport in summary.imports) { 276 | var import = getImportPath(jsImport, config, segments, packageLibDir); 277 | if (import == null) continue; 278 | extraImports.write('\n'); 279 | } 280 | 281 | var mainHtml = ''' 282 | 283 | $extraImports 284 | \n 285 | '''; 286 | createFile(path.join(outputDir, '$name.html')) 287 | ..createSync(recursive: true) 288 | ..writeAsStringSync(mainHtml); 289 | 290 | // Create the *_nodart.html file. This contains all the other html imports. 291 | var noDartExtraImports = 292 | extraImports.toString().replaceAll('.html', '_nodart.html'); 293 | var htmlBody = ''' 294 | 295 | $noDartExtraImports 296 | '''; 297 | createFile(path.join(outputDir, '${name}_nodart.html')) 298 | ..createSync(recursive: true) 299 | ..writeAsStringSync('$htmlBody'); 300 | } 301 | 302 | void _deleteFilesMatchingPatterns(List patterns) { 303 | new Directory(path.join('lib', 'src')) 304 | .listSync(recursive: true, followLinks: false) 305 | .where((file) => patterns.any((pattern) => path 306 | .relative(file.path, from: path.join('lib', 'src')) 307 | .contains(pattern))) 308 | .forEach((file) { 309 | if (file.existsSync()) file.deleteSync(recursive: true); 310 | }); 311 | } 312 | 313 | int _lastLength = 0; 314 | _progress(String msg) { 315 | const ESC = '\x1b'; 316 | stdout.write('\r$ESC[32m$msg$ESC[0m'); 317 | var len = msg.length; 318 | if (len < _lastLength && !verbose) { 319 | stdout.write(' ' * (_lastLength - len)); 320 | } 321 | _lastLength = len; 322 | } 323 | 324 | bool _progressLineBroken = false; 325 | _showMessage(String msg) { 326 | if (!verbose) return; 327 | if (!_progressLineBroken) { 328 | _progressLineBroken = true; 329 | stdout.write('\n'); 330 | } 331 | print(msg); 332 | } 333 | 334 | const String EMPTY_SCRIPT_WORKAROUND_ISSUE_11 = ''' 335 | '''; 339 | -------------------------------------------------------------------------------- /lib/src/codegen.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The Polymer Project Authors. All rights reserved. 2 | // This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 3 | // The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 4 | // The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 5 | // Code distributed by Google as part of the polymer project is also 6 | // subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 7 | 8 | /// Methods to generate code from previously collected information. 9 | library custom_element_apigen.src.codegen; 10 | 11 | import 'package:path/path.dart' as path; 12 | import 'html_element_names.dart'; 13 | 14 | import 'config.dart'; 15 | import 'ast.dart'; 16 | 17 | String generateClass(Class classSummary, FileConfig config, 18 | Map elementSummaries, Map mixinSummaries) { 19 | var sb = new StringBuffer(); 20 | var comment = _toComment(classSummary.description); 21 | var baseExtendName; 22 | if (classSummary is Element) { 23 | baseExtendName = _baseExtendName(classSummary.extendName, elementSummaries); 24 | sb.write(_generateElementHeader(classSummary.name, comment, 25 | classSummary.extendName, baseExtendName, classSummary.mixins, 26 | mixinSummaries)); 27 | } else if (classSummary is Mixin) { 28 | sb.write(_generateMixinHeader(classSummary, comment, mixinSummaries)); 29 | } else { 30 | throw 'unsupported summary type: $classSummary'; 31 | } 32 | 33 | var getDartName = _substituteFunction(config.nameSubstitutions); 34 | classSummary.properties.values 35 | .forEach((p) => _generateProperty(p, sb, getDartName)); 36 | classSummary.methods.forEach((m) => _generateMethod(m, sb, getDartName)); 37 | sb.write('}\n'); 38 | return sb.toString(); 39 | } 40 | 41 | String _baseExtendName(String extendName, Map allElements) { 42 | if (extendName == null || extendName.isEmpty) return null; 43 | var baseExtendName = extendName; 44 | var baseExtendElement = allElements[baseExtendName]; 45 | while (baseExtendElement != null && 46 | baseExtendElement.extendName != null && 47 | !baseExtendElement.extendName.isEmpty) { 48 | baseExtendName = baseExtendElement.extendName; 49 | baseExtendElement = allElements[baseExtendName]; 50 | } 51 | return baseExtendName; 52 | } 53 | 54 | Function _substituteFunction(Map nameSubstitutions) { 55 | if (nameSubstitutions == null) return (x) => x; 56 | return (x) { 57 | var v = nameSubstitutions[x]; 58 | return v != null ? v : x; 59 | }; 60 | } 61 | 62 | const _propertiesToSkip = const [ 63 | 'properties', 64 | 'listeners', 65 | 'observers', 66 | 'hostAttributes' 67 | ]; 68 | 69 | void _generateProperty( 70 | Property property, StringBuffer sb, String getDartName(String)) { 71 | // Don't add these to the generated classes, they are not meant to be called 72 | // directly. 73 | if (_propertiesToSkip.contains(property.name)) return; 74 | 75 | var comment = _toComment(property.description, 2); 76 | var type = property.type; 77 | if (type != null) { 78 | type = _docToDartType[type.toLowerCase()]; 79 | } 80 | var name = property.name; 81 | var dartName = getDartName(name); 82 | var body = "jsElement[r'$name']"; 83 | sb.write(comment == '' ? '\n' : '\n$comment\n'); 84 | var t = type != null ? '$type ' : ''; 85 | 86 | // Write the getter if one exists. 87 | if (property.hasGetter) { 88 | sb.write(' ${t}get $dartName => $body;\n'); 89 | } 90 | 91 | // Write the setter if one exists. 92 | if (property.hasSetter) { 93 | if (type == null) { 94 | sb.write(' set $dartName(${t}value) { ' 95 | '$body = (value is Map || (value is Iterable && value is! JsArray)) ' 96 | '? new JsObject.jsify(value) : value;}\n'); 97 | } else if (type == "List") { 98 | sb.write(' set $dartName(${t}value) { ' 99 | '$body = (value != null && value is! JsArray) ? ' 100 | 'new JsObject.jsify(value) : value;}\n'); 101 | } else { 102 | sb.write(' set $dartName(${t}value) { $body = value; }\n'); 103 | } 104 | } 105 | } 106 | 107 | const _methodsToSkip = const [ 108 | 'created', 109 | 'attached', 110 | 'detached', 111 | 'ready', 112 | 'attributeChanged' 113 | ]; 114 | 115 | void _generateMethod( 116 | Method method, StringBuffer sb, String getDartName(String)) { 117 | // Don't add these to the generated classes, they are not meant to be called 118 | // directly. 119 | if (_methodsToSkip.contains(method.name)) return; 120 | 121 | var comment = _toComment(method.description, 2); 122 | sb.write(comment == '' ? '\n' : '\n$comment\n'); 123 | for (var arg in method.args) { 124 | _generateArgComment(arg, sb); 125 | } 126 | for (var arg in method.optionalArgs) { 127 | _generateArgComment(arg, sb); 128 | } 129 | sb.write(' '); 130 | var type = 131 | method.type != null ? _docToDartType[method.type.toLowerCase()] : null; 132 | if (type != null) { 133 | sb.write('$type '); 134 | } 135 | var name = method.name; 136 | var dartName = getDartName(name); 137 | sb.write('$dartName('); 138 | var argList = new StringBuffer(); 139 | // First do the regular args, then the optional ones if there are any. 140 | _generateArgList(method.args, sb, argList); 141 | if (!method.optionalArgs.isEmpty) { 142 | if (!method.args.isEmpty) { 143 | sb.write(', '); 144 | argList.write(', '); 145 | } 146 | sb.write('['); 147 | _generateArgList(method.optionalArgs, sb, argList); 148 | sb.write(']'); 149 | } 150 | 151 | sb.write(") =>\n jsElement.callMethod('$name', [$argList]);\n"); 152 | } 153 | 154 | // Returns whether it found any args or not. 155 | void _generateArgList( 156 | List args, StringBuffer dartArgList, StringBuffer jsArgList) { 157 | bool first = true; 158 | for (var arg in args) { 159 | if (!first) { 160 | dartArgList.write(', '); 161 | jsArgList.write(', '); 162 | } 163 | first = false; 164 | var type = arg.type; 165 | if (type != null) { 166 | type = _docToDartType[type.toLowerCase()]; 167 | } 168 | if (type != null) { 169 | dartArgList 170 | ..write(type) 171 | ..write(' '); 172 | } 173 | dartArgList.write(arg.name); 174 | jsArgList.write(arg.name); 175 | } 176 | } 177 | 178 | String generateDirectives(String name, List segments, 179 | FileSummary summary, FileConfig config, String packageLibDir, 180 | Map mixinSummaries) { 181 | var libName = path 182 | .withoutExtension(segments.map((s) => s.replaceAll('-', '_')).join('.')); 183 | var elementName = name.replaceAll('-', '_'); 184 | var extraImports = new Set(); 185 | if (config.extraImports!=null) { 186 | config.extraImports.forEach((pkg) { 187 | extraImports.add("import '$pkg';"); 188 | }); 189 | } 190 | 191 | // Given a mixin, adds imports for it and all its recursive dependencies. 192 | addMixinImports(String mixinName) { 193 | var import = _generateMixinImport( 194 | mixinName, config, mixinSummaries, packageLibDir); 195 | if (import != null) extraImports.add(import); 196 | 197 | // Add imports for things each mixin `extends`. 198 | var mixin = _getMixinOrDie(mixinName, mixinSummaries); 199 | if (mixin.additionalMixins == null) return; 200 | for (var mixinName in mixin.additionalMixins) { 201 | addMixinImports(mixinName); 202 | } 203 | } 204 | 205 | for (var element in summary.elements) { 206 | var extendName = element.extendName; 207 | if (extendName != null && extendName.contains('-')) { 208 | var extendsImport = config.extendsImport; 209 | if (extendsImport == null) { 210 | var packageName = config.global.findPackageNameForElement(extendName); 211 | var fileName = '${extendName.replaceAll('-', '_')}.dart'; 212 | extendsImport = 213 | packageName != null ? 'package:$packageName/$fileName' : fileName; 214 | } 215 | extraImports.add("import '$extendsImport';"); 216 | } 217 | 218 | for (var mixinName in element.mixins) { 219 | addMixinImports(mixinName); 220 | } 221 | } 222 | 223 | // Add imports for things each mixin `extends`. 224 | for (var mixin in summary.mixins) { 225 | if (mixin.additionalMixins == null) continue; 226 | for (var mixinName in mixin.additionalMixins) { 227 | addMixinImports(mixinName); 228 | } 229 | } 230 | 231 | for (var import in summary.imports) { 232 | var htmlImport = getImportPath(import, config, segments, packageLibDir, 233 | isDartFile: true); 234 | if (htmlImport == null) continue; 235 | var dartImport = '${path.withoutExtension(htmlImport)}.dart'; 236 | extraImports.add("import '$dartImport';"); 237 | } 238 | 239 | var packageName = config.global.currentPackage; 240 | var output = new StringBuffer(); 241 | output.write(''' 242 | // DO NOT EDIT: auto-generated with `pub run custom_element_apigen:update` 243 | 244 | /// Dart API for the polymer element `$name`. 245 | @HtmlImport('${elementName}_nodart.html') 246 | library $packageName.$libName; 247 | 248 | import 'dart:html'; 249 | import 'dart:js' show JsArray, JsObject; 250 | import 'package:web_components/web_components.dart'; 251 | import 'package:polymer_interop/polymer_interop.dart'; 252 | '''); 253 | extraImports.forEach((import) => output.writeln(import)); 254 | return output.toString(); 255 | } 256 | 257 | String getImportPath(Import import, FileConfig config, List segments, 258 | String packageLibDir, {bool isDartFile: false}) { 259 | var importPath = import.importPath; 260 | if (importPath == null || importPath.contains('polymer.html')) return null; 261 | var omit = config.omitImports; 262 | if (omit != null && omit.any((path) => importPath.contains(path))) { 263 | return null; 264 | } 265 | 266 | var importSegments = path.split(importPath); 267 | if (importSegments[0] == 'lib' && importSegments[1] == 'src') { 268 | importSegments.removeRange(0, 2); 269 | // If it lives in the top level dir of an element folder, put it in the top 270 | // level dir of lib. However, if its in a subdir of the element folder, then 271 | // we keep it as is. 272 | if (importSegments.length == 2) { 273 | importSegments.removeAt(0); 274 | } 275 | } 276 | var dartImport = path.joinAll(importSegments).replaceAll('-', '_'); 277 | var targetElement = importSegments.last; 278 | var packageName = config.global.findPackageNameForElement(targetElement); 279 | if (packageName != null) { 280 | if (isDartFile) { 281 | dartImport = 'package:$packageName/$dartImport'; 282 | } else { 283 | dartImport = path.join( 284 | '..', '..', packageLibDir, 'packages', packageName, dartImport); 285 | } 286 | } else { 287 | dartImport = path.join(packageLibDir, dartImport); 288 | } 289 | return dartImport; 290 | } 291 | 292 | String _generateMixinImport(String name, FileConfig config, 293 | Map mixinSummaries, String packageLibDir) { 294 | var filePath = _mixinImportPath(name, mixinSummaries, packageLibDir, config); 295 | if (filePath == null) return null; 296 | return "import '$filePath';"; 297 | } 298 | 299 | String _generateMixinHeader(Mixin summary, String comment, Map mixinSummaries) { 300 | var className = summary.name.split('.').last; 301 | var additional = new StringBuffer(); 302 | if (summary.extendName != null) additional.write(', ${summary.extendName}'); 303 | 304 | addMixins(String mixinName) { 305 | var mixin = _getMixinOrDie(mixinName, mixinSummaries); 306 | if (mixin.additionalMixins == null) return; 307 | 308 | for (var name in mixin.additionalMixins) { 309 | additional.write(', ${name}'); 310 | } 311 | } 312 | addMixins(summary.name); 313 | 314 | return ''' 315 | 316 | $comment 317 | @BehaviorProxy(const ['Polymer', '${summary.name}']) 318 | abstract class $className implements CustomElementProxyMixin$additional { 319 | '''; 320 | } 321 | 322 | String _generateElementHeader(String name, String comment, String extendName, 323 | String baseExtendName, List mixins, 324 | Map mixinSummaries) { 325 | var className = toCamelCase(name); 326 | 327 | var extendClassName; 328 | var hasCustomElementProxyMixin = false; 329 | if (extendName == null) { 330 | extendClassName = 331 | 'HtmlElement with CustomElementProxyMixin, PolymerBase'; 332 | hasCustomElementProxyMixin = true; 333 | } else if (!extendName.contains('-')) { 334 | extendClassName = '${HTML_ELEMENT_NAMES[baseExtendName]} with ' 335 | 'CustomElementProxyMixin, PolymerBase'; 336 | hasCustomElementProxyMixin = true; 337 | } else { 338 | extendClassName = toCamelCase(extendName); 339 | } 340 | 341 | var mixinNames = []; 342 | 343 | addMixinNames(String mixinName) { 344 | // Add imports for things each mixin `extends`. 345 | var mixin = _getMixinOrDie(mixinName, mixinSummaries); 346 | if (mixin.additionalMixins != null) { 347 | for (var name in mixin.additionalMixins) { 348 | addMixinNames(name); 349 | } 350 | } 351 | mixinNames.add(mixinName); 352 | } 353 | for (var mixin in mixins) { 354 | addMixinNames(mixin); 355 | } 356 | 357 | var optionalMixinString = mixinNames.isEmpty 358 | ? '' 359 | : '${hasCustomElementProxyMixin 360 | ? ', ' 361 | : ' with '}${mixinNames.join(', ')}'; 362 | 363 | var factoryMethod = new StringBuffer('factory ${className}() => '); 364 | if (baseExtendName == null || baseExtendName.contains('-')) { 365 | factoryMethod.write('new Element.tag(\'$name\');'); 366 | } else { 367 | factoryMethod.write('new Element.tag(\'$baseExtendName\', \'$name\');'); 368 | } 369 | 370 | var customElementProxy = _generateCustomElementProxy(name, baseExtendName); 371 | return ''' 372 | 373 | $comment 374 | $customElementProxy 375 | class $className extends $extendClassName$optionalMixinString { 376 | ${className}.created() : super.created(); 377 | $factoryMethod 378 | '''; 379 | } 380 | 381 | String _generateCustomElementProxy(String name, String baseExtendName) { 382 | var className = toCamelCase(name); 383 | // Only pass the extendsTag if its a native element. 384 | var maybeExtendsTag = ''; 385 | if (baseExtendName != null && !baseExtendName.contains('-')) { 386 | maybeExtendsTag = ', extendsTag: \'$baseExtendName\''; 387 | } 388 | return "@CustomElementProxy('$name'$maybeExtendsTag)"; 389 | } 390 | 391 | void _generateArgComment(Argument arg, StringBuffer sb) { 392 | var name = arg.name; 393 | if (arg.description == null) return; 394 | var description = arg.description.trim(); 395 | if (description == '') return; 396 | var comment = description.replaceAll('\n', '\n /// '); 397 | sb.write(' /// [${name}]: $comment\n'); 398 | } 399 | 400 | String _toComment(String description, [int indent = 0]) { 401 | if (description == null) return ''; 402 | description = description.trim(); 403 | if (description == '') return ''; 404 | var s1 = ' ' * indent; 405 | var comment = description.split('\n').map((e) { 406 | var trimmed = e.trimRight(); 407 | return trimmed == '' ? '' : ' $trimmed'; 408 | }).join('\n$s1///'); 409 | return '$s1///$comment'; 410 | } 411 | 412 | String toCamelCase(String dashName) => dashName 413 | .split('-') 414 | .map((e) => '${e[0].toUpperCase()}${e.substring(1)}') 415 | .join(''); 416 | 417 | String _mixinImportPath(String className, Map mixinSummaries, 418 | String packageLibDir, FileConfig config) { 419 | var mixin = _getMixinOrDie(className, mixinSummaries); 420 | var fileSummary = mixin.summary; 421 | assert(fileSummary != null); 422 | 423 | // Don't include omitted imports 424 | var omit = config.omitImports; 425 | if (omit != null && omit.any((path) => fileSummary.path.contains(path))) { 426 | return null; 427 | } 428 | 429 | var parts = path.split(fileSummary.path); 430 | // Check for `packages` imports. 431 | if (parts[0] == 'packages') { 432 | return fileSummary.path.replaceFirst('packages/', 'package:').replaceFirst( 433 | '.html', '.dart'); 434 | } 435 | 436 | var libPath; 437 | if (parts.length == 4) { 438 | libPath = parts.last; 439 | } else { 440 | libPath = path.joinAll(parts.getRange(2, parts.length)); 441 | } 442 | libPath = libPath.replaceAll('-', '_').replaceFirst('.html', '.dart'); 443 | return '$packageLibDir$libPath'; 444 | } 445 | 446 | Mixin _getMixinOrDie(String name, Map summaries) { 447 | var mixin = summaries[name]; 448 | if (mixin == null) { 449 | throw 'Unable to find mixin $name. Make sure the mixin file is ' 450 | 'loaded. If you don\'t want to generate the mixin as a dart api ' 451 | 'then you can use the `files_to_load` section to load it.'; 452 | } 453 | return mixin; 454 | } 455 | 456 | final _docToDartType = { 457 | 'boolean': 'bool', 458 | 'array': 'List', 459 | 'string': 'String', 460 | 'number': 'num', 461 | 'object': null, // keep as dynamic 462 | 'any': null, // keep as dynamic 463 | 'element': 'Element', 464 | 'null': null 465 | }; 466 | --------------------------------------------------------------------------------