From b1562cd74c8b7332c82bbdeaea0dbd2df8e33705 Mon Sep 17 00:00:00 2001 From: Simon Bailey Date: Tue, 7 Jan 2014 20:00:49 +0000 Subject: [PATCH] Basic project generator setup TODO: - App tests - Generator tests - Component generator Signed-off-by: Simon Bailey --- .editorconfig | 13 + .gitattributes | 1 + .gitignore | 31 + .jshintrc | 21 + .travis.yml | 8 + LICENSE | 20 + README.md | 47 + app/index.js | 74 + app/templates/images/yeoman.png | Bin 0 -> 13501 bytes app/templates/styles/main.css | 41 + app/templates/styles/reset.css | 48 + common/index.js | 17 + main/index.js | 13 + package.json | 41 + script-base.js | 61 + templates/common/Gruntfile.js | 137 + templates/common/_package.json | 36 + templates/common/gitignore | 32 + templates/common/index.html | 19 + templates/common/root/.editorconfig | 13 + templates/common/root/.jshintrc | 27 + templates/common/root/src/favicon.ico | Bin 0 -> 4286 bytes templates/common/root/test/.jshintrc | 40 + .../root/test/helpers/phantomjs-shims.js | 34 + .../common/root/test/helpers/react/addons.js | 16336 ++++++++++++++++ templates/javascript/App.js | 32 + test/test-creation.js | 38 + test/test-load.js | 11 + util.js | 80 + 29 files changed, 17271 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/index.js create mode 100644 app/templates/images/yeoman.png create mode 100644 app/templates/styles/main.css create mode 100644 app/templates/styles/reset.css create mode 100644 common/index.js create mode 100644 main/index.js create mode 100644 package.json create mode 100644 script-base.js create mode 100644 templates/common/Gruntfile.js create mode 100644 templates/common/_package.json create mode 100644 templates/common/gitignore create mode 100644 templates/common/index.html create mode 100644 templates/common/root/.editorconfig create mode 100644 templates/common/root/.jshintrc create mode 100644 templates/common/root/src/favicon.ico create mode 100644 templates/common/root/test/.jshintrc create mode 100644 templates/common/root/test/helpers/phantomjs-shims.js create mode 100755 templates/common/root/test/helpers/react/addons.js create mode 100644 templates/javascript/App.js create mode 100644 test/test-creation.js create mode 100644 test/test-load.js create mode 100644 util.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c308ed0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d1ef45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +### SublimeText ### +*.sublime-workspace + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# App specific + +node_modules/ +temp/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..da64b6e --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "node": true, + "esnext": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3f16ce8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: + - '0.8' + - '0.10' +before_install: + - currentfolder=${PWD##*/} + - if [ "$currentfolder" != 'generator-react-webpack' ]; then cd .. && eval "mv $currentfolder generator-react-webpack" && cd generator-react-webpack; fi + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5e8019 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright 2014 Simon Bailey + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e83623c --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# generator-react-webpack [![Build Status](https://secure.travis-ci.org/newtriks/generator-react-webpack.png?branch=master)](https://travis-ci.org/newtriks/generator-react-webpack) + +A generator for [Yeoman](http://yeoman.io). + + +## Getting Started + +### What is Yeoman? + +Trick question. It's not a thing. It's this guy: + +![](http://i.imgur.com/JHaAlBJ.png) + +Basically, he wears a top hat, lives in your computer, and waits for you to tell him what kind of application you wish to create. + +Not every new computer comes with a Yeoman pre-installed. He lives in the [npm](https://npmjs.org) package repository. You only have to ask for him once, then he packs up and moves into your hard drive. *Make sure you clean up, he likes new and shiny things.* + +``` +$ npm install -g yo +``` + +### Yeoman Generators + +Yeoman travels light. He didn't pack any generators when he moved in. You can think of a generator like a plug-in. You get to choose what type of application you wish to create, such as a Backbone application or even a Chrome extension. + +To install generator-react-webpack from npm, run: + +``` +$ npm install -g generator-react-webpack +``` + +Finally, initiate the generator: + +``` +$ yo react-webpack +``` + +### Getting To Know Yeoman + +Yeoman has a heart of gold. He's a person with feelings and opinions, but he's very easy to work with. If you think he's too opinionated, he can be easily convinced. + +If you'd like to get to know Yeoman better and meet some of his friends, [Grunt](http://gruntjs.com) and [Bower](http://bower.io), check out the complete [Getting Started Guide](https://github.com/yeoman/yeoman/wiki/Getting-Started). + + +## License + +[MIT License](http://en.wikipedia.org/wiki/MIT_License) diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..740dc80 --- /dev/null +++ b/app/index.js @@ -0,0 +1,74 @@ +'use strict'; +var util = require('util'); +var path = require('path'); +var yeoman = require('yeoman-generator'); +var generalUtils = require('../util.js'); + +var ReactWebpackGenerator = module.exports = function ReactWebpackGenerator(args, options, config) { + yeoman.generators.Base.apply(this, arguments); + + this.argument('appname', { type: String, required: false }); + this.appname = this.appname || path.basename(process.cwd()); + this.appname = this._.camelize(this._.slugify(this._.humanize(this.appname))); + this.scriptAppName = this._.capitalize(this.appname) + generalUtils.appName(this); + + args = ['main']; + + if (typeof this.env.options.appPath === 'undefined') { + this.env.options.appPath = this.env.options.appPath || 'src'; + } + + this.appPath = this.env.options.appPath; + + this.hookFor('react-webpack:common', { + args: args + }); + + this.hookFor('react-webpack:main', { + args: args + }); + + this.on('end', function () { + this.installDependencies({ skipInstall: options['skip-install'] }); + }); + + this.pkg = JSON.parse(this.readFileAsString(path.join(__dirname, '../package.json'))); +}; + +util.inherits(ReactWebpackGenerator, yeoman.generators.Base); + +ReactWebpackGenerator.prototype.welcome = function welcome() { + // welcome message + if (!this.options['skip-welcome-message']) { + console.log(this.yeoman); + console.log( + 'Out of the box I include Webpack and some default React components.\n' + ); + } +}; + +ReactWebpackGenerator.prototype.readIndex = function readIndex() { + this.indexFile = this.engine(this.read('../../templates/common/index.html'), this); +}; + +ReactWebpackGenerator.prototype.createIndexHtml = function createIndexHtml() { + this.indexFile = this.indexFile.replace(/'/g, "'"); + this.write(path.join(this.appPath, 'index.html'), this.indexFile); +}; + +ReactWebpackGenerator.prototype.packageFiles = function () { + this.template('../../templates/common/_package.json', 'package.json'); + this.copy('../../templates/common/Gruntfile.js', 'Gruntfile.js'); +}; + +ReactWebpackGenerator.prototype.styleFiles = function styleFiles() { + var mainFile = 'main.css'; + var resetFile = 'reset.css'; + this.copy('styles/' + mainFile, 'src/styles/' + mainFile); + this.copy('styles/' + resetFile, 'src/styles/' + resetFile); +}; + +ReactWebpackGenerator.prototype.imageFiles = function () { + this.sourceRoot(path.join(__dirname, 'templates')); + this.directory('images', 'src/images', true); +}; diff --git a/app/templates/images/yeoman.png b/app/templates/images/yeoman.png new file mode 100644 index 0000000000000000000000000000000000000000..92497addf96c5c009ac24007f55eb989f3bf74c6 GIT binary patch literal 13501 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 literal 0 HcmV?d00001 diff --git a/app/templates/styles/main.css b/app/templates/styles/main.css new file mode 100644 index 0000000..321e9a1 --- /dev/null +++ b/app/templates/styles/main.css @@ -0,0 +1,41 @@ +/* Stiziles */ + +html, body { + background: #222222; +} + +/* main */ + +.main { + width: 100%; + height: 100%; + background: #222222; + color: #fff; +} + +.main img { + width: 103px; + height: 89px; + margin-bottom: 10px; + text-align: center; +} + +/* transitions */ + +.fade-enter { + opacity: 0.01; + transition: opacity .5s ease-in; +} + +.fade-enter.fade-enter-active { + opacity: 1; +} + +.fade-leave { + opacity: 1; + transition: opacity .5s ease-in; +} + +.fade-leave.fade-leave-active { + opacity: 0.01; +} diff --git a/app/templates/styles/reset.css b/app/templates/styles/reset.css new file mode 100644 index 0000000..af94440 --- /dev/null +++ b/app/templates/styles/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/common/index.js b/common/index.js new file mode 100644 index 0000000..0452f1a --- /dev/null +++ b/common/index.js @@ -0,0 +1,17 @@ +'use strict'; +var path = require('path'); +var util = require('util'); +var yeoman = require('yeoman-generator'); + +var CommonGenerator = module.exports = function CommonGenerator(args, options, config) { + yeoman.generators.NamedBase.apply(this, arguments); +}; + +util.inherits(CommonGenerator, yeoman.generators.NamedBase); + +CommonGenerator.prototype.setupEnv = function setupEnv() { + // Copies the contents of the generator `templates` + // directory into your users new application path + this.sourceRoot(path.join(__dirname, '../templates/common')); + this.directory('root', '.', true); +}; diff --git a/main/index.js b/main/index.js new file mode 100644 index 0000000..3b39a36 --- /dev/null +++ b/main/index.js @@ -0,0 +1,13 @@ +'use strict'; +var util = require('util'); +var ScriptBase = require('../script-base.js'); + +var MainGenerator = module.exports = function MainGenerator(args, options, config) { + ScriptBase.apply(this, arguments); +}; + +util.inherits(MainGenerator, ScriptBase); + +MainGenerator.prototype.createAppFile = function createAppFile() { + this.appTemplate('app', 'scripts/components/'+this.scriptAppName); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbe5ca8 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "generator-react-webpack", + "version": "0.0.0", + "description": "A generator for Yeoman", + "keywords": [ + "yeoman-generator" + ], + "homepage": "https://github.com/newtriks/generator-react-webpack", + "bugs": "https://github.com/newtriks/generator-react-webpack/issues", + "author": { + "name": "Simon Bailey", + "email": "simon@newtriks.com", + "url": "https://github.com/newtriks" + }, + "main": "app/index.js", + "repository": { + "type": "git", + "url": "git://github.com/newtriks/generator-react-webpack.git" + }, + "scripts": { + "test": "mocha" + }, + "dependencies": { + "yeoman-generator": "~0.14.0" + }, + "devDependencies": { + "mocha": "~1.14.0" + }, + "peerDependencies": { + "yo": ">=1.0.0" + }, + "engines": { + "node": ">=0.8.0", + "npm": ">=1.2.10" + }, + "licenses": [ + { + "type": "MIT" + } + ] +} diff --git a/script-base.js b/script-base.js new file mode 100644 index 0000000..261047c --- /dev/null +++ b/script-base.js @@ -0,0 +1,61 @@ +'use strict'; +var util = require('util'); +var path = require('path'); +var yeoman = require('yeoman-generator'); +var generalUtils = require('./util.js'); + +var Generator = module.exports = function Generator() { + yeoman.generators.NamedBase.apply(this, arguments); + + // Add capitalize mixin + this._.mixin({ 'capitalize': generalUtils.capitalize }); + + this.appname = path.basename(process.cwd()); + + this.appname = this._.slugify(this._.humanize(this.appname)); + this.scriptAppName = this._.capitalize(this.appname) + generalUtils.appName(this); + + if (typeof this.env.options.appPath === 'undefined') { + this.env.options.appPath = this.env.options.appPath || 'src'; + } + + if (typeof this.env.options.testPath === 'undefined') { + this.env.options.testPath = this.env.options.testPath || 'test/spec'; + } + + var sourceRoot = '/templates/javascript'; + this.scriptSuffix = '.js'; + + this.sourceRoot(path.join(__dirname, sourceRoot)); +}; + +util.inherits(Generator, yeoman.generators.NamedBase); + +Generator.prototype.appTemplate = function (src, dest) { + yeoman.generators.Base.prototype.template.apply(this, [ + src + this.scriptSuffix, + path.join(this.env.options.appPath, dest) + this.scriptSuffix + ]); +}; + +Generator.prototype.testTemplate = function (src, dest) { + yeoman.generators.Base.prototype.template.apply(this, [ + src + this.scriptSuffix, + path.join(this.env.options.testPath, dest) + this.scriptSuffix + ]); +}; + +Generator.prototype.htmlTemplate = function (src, dest) { + yeoman.generators.Base.prototype.template.apply(this, [ + src, + path.join(this.env.options.appPath, dest.toLowerCase()) + ]); +}; + +Generator.prototype.generateSourceAndTest = function (appTemplate, testTemplate, targetDirectory, skipAdd) { + this.appTemplate(appTemplate, path.join('scripts', targetDirectory, this.name)); + this.testTemplate(testTemplate, path.join(targetDirectory, this.name)); + if (!skipAdd) { + this.addScriptToIndex(path.join(targetDirectory, this.name)); + } +}; \ No newline at end of file diff --git a/templates/common/Gruntfile.js b/templates/common/Gruntfile.js new file mode 100644 index 0000000..3b10070 --- /dev/null +++ b/templates/common/Gruntfile.js @@ -0,0 +1,137 @@ +'use strict'; +var LIVERELOAD_PORT = 35729; +var lrSnippet = require('connect-livereload')({ + port: LIVERELOAD_PORT +}); +var mountFolder = function (connect, dir) { + return connect.static(require('path').resolve(dir)); +}; + +module.exports = function (grunt) { + // Let *load-grunt-tasks* require everything + require('load-grunt-tasks')(grunt); + // Read configuration from package.json + var pkgConfig = grunt.file.readJSON('package.json'); + var jshintConfig = grunt.file.readJSON('.jshintrc'); + + var loaders = [{ + test: /\.css$/, + loader: 'style!css' + }, { + test: /\.gif/, + loader: 'url-loader?limit=10000&minetype=image/gif' + }, { + test: /\.jpg/, + loader: 'url-loader?limit=10000&minetype=image/jpg' + }, { + test: /\.png/, + loader: 'url-loader?limit=10000&minetype=image/png' + }, { + test: /\.js$/, + loader: 'jsx-loader' + }]; + + grunt.initConfig({ + pkg: pkgConfig, + webpack: { + development: { + entry: './<%= pkg.src %>/scripts/components/<%= pkg.mainInput %>.js', + output: { + path: '<%= pkg.src %>/scripts/', + filename: '<%= pkg.mainOutput %>.js' + }, + debug: true, + cache: true, + stats: { + colors: true, + reasons: true + }, + jshint: grunt.util._.merge(jshintConfig, { + emitErrors: false, + failOnHint: false + }), + module: { + preLoaders: [{ + test: '\\.js$', + exclude: 'node_modules', + loader: 'jshint' + }], + loaders: loaders + } + } + }, + watch: { + webpack: { + files: ['<%= pkg.src %>/scripts/{,*/}*.js', + '<%= pkg.src %>/styles/{,*/}*.css', + '!<%= pkg.src %>/scripts/<%= pkg.mainOutput %>.js' + ], + tasks: ['webpack:development'] + }, + livereload: { + options: { + livereload: LIVERELOAD_PORT + }, + files: [ + '<%= pkg.src %>/{,*/}*.html', + '<%= pkg.src %>/scripts/<%= pkg.mainOutput %>.js' + ] + } + }, + connect: { + options: { + port: 8000, + // Change this to '0.0.0.0' to access the server from outside. + hostname: 'localhost' + }, + livereload: { + options: { + middleware: function (connect) { + return [ + lrSnippet, + mountFolder(connect, pkgConfig.src) + ]; + } + } + }, + dist: { + options: { + middleware: function (connect) { + return [ + mountFolder(connect, pkgConfig.dist) + ]; + } + } + } + }, + open: { + server: { + url: 'http://localhost:<%= connect.options.port %>' + } + }, + karma: { + unit: { + configFile: 'karma.conf.js' + } + } + }); + + grunt.registerTask('server', function (target) { + if (target === 'dist') { + return grunt.task.run(['build', 'open', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'connect:livereload', + 'webpack:development', + 'open', + 'watch' + ]); + }); + + grunt.registerTask('test', ['karma']); + + grunt.registerTask('build', []); + + grunt.registerTask('default', []); +}; diff --git a/templates/common/_package.json b/templates/common/_package.json new file mode 100644 index 0000000..7a0a648 --- /dev/null +++ b/templates/common/_package.json @@ -0,0 +1,36 @@ +{ + "name": "<%= _.slugify(appname) %>", + "version": "0.0.0", + "src": "src", + "test": "test", + "dist": "dist", + "mainInput": "<%= scriptAppName %>", + "mainOutput": "main", + "dependencies": { + "react": "~0.8.0" + }, + "devDependencies": { + "grunt": "~0.4.2", + "load-grunt-tasks": "~0.2.1", + "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-connect": "~0.6.0", + "connect-livereload": "~0.3.1", + "webpack": "~1.0.0-beta4", + "jsx-loader": "petehunt/jsx-loader", + "grunt-webpack": "~1.0.0", + "style-loader": "~0.6.0", + "url-loader": "~0.5.2", + "css-loader": "~0.6.6", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.2", + "karma-firefox-launcher": "~0.1.3", + "karma-jasmine": "~0.1.5", + "karma-phantomjs-launcher": "~0.1.1", + "karma": "~0.10.9", + "grunt-karma": "~0.6.2", + "karma-webpack-plugin": "~1.0.0", + "webpack-dev-server": "~1.0.2", + "grunt-open": "~0.2.2", + "jshint-loader": "~0.6.0" + } +} diff --git a/templates/common/gitignore b/templates/common/gitignore new file mode 100644 index 0000000..cfb1a09 --- /dev/null +++ b/templates/common/gitignore @@ -0,0 +1,32 @@ +### SublimeText ### +*.sublime-workspace + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride +Icon + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# App specific + +node_modules/ +.tmp +/src/scripts/main.js diff --git a/templates/common/index.html b/templates/common/index.html new file mode 100644 index 0000000..71fa2a1 --- /dev/null +++ b/templates/common/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + +
+

If you can see this, something is broken (or JS is not enabled).

+
+ + + diff --git a/templates/common/root/.editorconfig b/templates/common/root/.editorconfig new file mode 100644 index 0000000..c308ed0 --- /dev/null +++ b/templates/common/root/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/templates/common/root/.jshintrc b/templates/common/root/.jshintrc new file mode 100644 index 0000000..b5f6ae1 --- /dev/null +++ b/templates/common/root/.jshintrc @@ -0,0 +1,27 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "camelcase": false, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": false, + "strict": true, + "trailing": true, + "smarttabs": true, + "white": true, + "newcap": false, + "globals": { + "React": true + } +} + diff --git a/templates/common/root/src/favicon.ico b/templates/common/root/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6527905307f19ba00762f9241f7eb535fa84a2f9 GIT binary patch literal 4286 zcmchaPe@cz6vpqQW1y54B@{_hhFD-kWPgyXjSGVaf);_51TESOlSPOdvy}@W5Q+** zs6~RrtlR}7(V|sCkP&1f7!5{Hixw@4+x@+HXSm*Z^WGalm2d8S=brO@=iGm9MyZ7P zPo)%}YN|=8W~EfSfibDm2H3qnGq$y%h@zqVv#zn@@WvhIGJ8*ECePe@roq(*vwGys z4?Q;bI~MRIM&jXu6Yg@wqQ#8&8x#z55E}ONd3<&rw_h!5AbBx{CcZ%&z736jHxFa0 zsBLqly3+dQ%MZGH{QU}GW6bsq=@$a@sXtac^<8>8uP>*+d!Qdtv&&mnKlvE_T-+SC z*QNCVwcvq%+&DDc+T}Uf(2_FavDN{-&hCpIs?aW=A$mcrzyD+9(025i1~K&uVf&w4 zItQLK9T{7k?s@bnU*&p+<^UI*aHA1aH+Fo^PAzM|xjNK09?2V(Cme7IFB(BP?7#at z(>DB3w`AUFS~=(LUBdZ>v-SG4J~%Mrfj&05Z)oj13l5tbEq4x>8+;FC0Dvr zbJY#7PS$+yE_Cf7gxqQEC@RoZX5J^}71l+`Q~qnOF4D za`lhjUuqZa-sj)EHDleV2i|mc!Ly-@7IwzPM{?pBUt(+@IHi8HTz#Iq9)9h|hrL3) zfOT#@|5$JCxmRjsOj>&kUt(m8*57|W(FoE`CX*8edYv%j=3sR5>!hvglJ#@8K6j$g z&IuUbRC_{)p}sbyx%UD6Fki;t6nDk0gT5&6Q_at7FbVVOu?4VK{oR#!kyYbCc;<4+LITzoZ8-~O5L+9MiLHL4NyME>! z;Ky7<)UR!gN_~GXhMvPMHNB;EmmIK}eHD&~cRx89jth}IM#tU%ablw0|GxfE9IjRR zl-)b-IvC#UD!IewzPL77SI>R+?}<2ERr|R2o~zCC8rJUR8>DI5*0O$6+k~wZ)Mt;b z(Hul-OFl+F))}lK&&Yi*+S2kJmHDbdBWOQnaSA6S|#* -1; +} + +var CSSCore = { + + /** + * Adds the class passed in to the element if it doesn't already have it. + * + * @param {DOMElement} element the element to set the class on + * @param {string} className the CSS className + * @return {DOMElement} the element passed in + */ + addClass: function(element, className) { + ("production" !== "development" ? invariant( + !/\s/.test(className), + 'CSSCore.addClass takes only a single class name. "%s" contains ' + + 'multiple classes.', className + ) : invariant(!/\s/.test(className))); + + if (className) { + if (element.classList) { + element.classList.add(className); + } else if (!hasClass(element, className)) { + element.className = element.className + ' ' + className; + } + } + return element; + }, + + /** + * Removes the class passed in from the element + * + * @param {DOMElement} element the element to set the class on + * @param {string} className the CSS className + * @return {DOMElement} the element passed in + */ + removeClass: function(element, className) { + ("production" !== "development" ? invariant( + !/\s/.test(className), + 'CSSCore.removeClass takes only a single class name. "%s" contains ' + + 'multiple classes.', className + ) : invariant(!/\s/.test(className))); + + if (className) { + if (element.classList) { + element.classList.remove(className); + } else if (hasClass(element, className)) { + element.className = element.className + .replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), '$1') + .replace(/\s+/g, ' ') // multiple spaces to one + .replace(/^\s*|\s*$/g, ''); // trim the ends + } + } + return element; + }, + + /** + * Helper to add or remove a class from an element based on a condition. + * + * @param {DOMElement} element the element to set the class on + * @param {string} className the CSS className + * @param {*} bool condition to whether to add or remove the class + * @return {DOMElement} the element passed in + */ + conditionClass: function(element, className, bool) { + return (bool ? CSSCore.addClass : CSSCore.removeClass)(element, className); + } +}; + +module.exports = CSSCore; + +},{"./invariant":113}],3:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CSSProperty + */ + +"use strict"; + +/** + * CSS properties which accept numbers but are not in units of "px". + */ +var isUnitlessNumber = { + fillOpacity: true, + fontWeight: true, + lineHeight: true, + opacity: true, + orphans: true, + zIndex: true, + zoom: true +}; + +/** + * Most style properties can be unset by doing .style[prop] = '' but IE8 + * doesn't like doing that with shorthand properties so for the properties that + * IE8 breaks on, which are listed here, we instead unset each of the + * individual properties. See http://bugs.jquery.com/ticket/12385. + * The 4-value 'clock' properties like margin, padding, border-width seem to + * behave without any problems. Curiously, list-style works too without any + * special prodding. + */ +var shorthandPropertyExpansions = { + background: { + backgroundImage: true, + backgroundPosition: true, + backgroundRepeat: true, + backgroundColor: true + }, + border: { + borderWidth: true, + borderStyle: true, + borderColor: true + }, + borderBottom: { + borderBottomWidth: true, + borderBottomStyle: true, + borderBottomColor: true + }, + borderLeft: { + borderLeftWidth: true, + borderLeftStyle: true, + borderLeftColor: true + }, + borderRight: { + borderRightWidth: true, + borderRightStyle: true, + borderRightColor: true + }, + borderTop: { + borderTopWidth: true, + borderTopStyle: true, + borderTopColor: true + }, + font: { + fontStyle: true, + fontVariant: true, + fontWeight: true, + fontSize: true, + lineHeight: true, + fontFamily: true + } +}; + +var CSSProperty = { + isUnitlessNumber: isUnitlessNumber, + shorthandPropertyExpansions: shorthandPropertyExpansions +}; + +module.exports = CSSProperty; + +},{}],4:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CSSPropertyOperations + * @typechecks static-only + */ + +"use strict"; + +var CSSProperty = require("./CSSProperty"); + +var dangerousStyleValue = require("./dangerousStyleValue"); +var escapeTextForBrowser = require("./escapeTextForBrowser"); +var hyphenate = require("./hyphenate"); +var memoizeStringOnly = require("./memoizeStringOnly"); + +var processStyleName = memoizeStringOnly(function(styleName) { + return escapeTextForBrowser(hyphenate(styleName)); +}); + +/** + * Operations for dealing with CSS properties. + */ +var CSSPropertyOperations = { + + /** + * Serializes a mapping of style properties for use as inline styles: + * + * > createMarkupForStyles({width: '200px', height: 0}) + * "width:200px;height:0;" + * + * Undefined values are ignored so that declarative programming is easier. + * + * @param {object} styles + * @return {?string} + */ + createMarkupForStyles: function(styles) { + var serialized = ''; + for (var styleName in styles) { + if (!styles.hasOwnProperty(styleName)) { + continue; + } + var styleValue = styles[styleName]; + if (styleValue != null) { + serialized += processStyleName(styleName) + ':'; + serialized += dangerousStyleValue(styleName, styleValue) + ';'; + } + } + return serialized || null; + }, + + /** + * Sets the value for multiple styles on a node. If a value is specified as + * '' (empty string), the corresponding style property will be unset. + * + * @param {DOMElement} node + * @param {object} styles + */ + setValueForStyles: function(node, styles) { + var style = node.style; + for (var styleName in styles) { + if (!styles.hasOwnProperty(styleName)) { + continue; + } + var styleValue = dangerousStyleValue(styleName, styles[styleName]); + if (styleValue) { + style[styleName] = styleValue; + } else { + var expansion = CSSProperty.shorthandPropertyExpansions[styleName]; + if (expansion) { + // Shorthand property that IE8 won't like unsetting, so unset each + // component to placate it + for (var individualStyleName in expansion) { + style[individualStyleName] = ''; + } + } else { + style[styleName] = ''; + } + } + } + } + +}; + +module.exports = CSSPropertyOperations; + +},{"./CSSProperty":3,"./dangerousStyleValue":97,"./escapeTextForBrowser":99,"./hyphenate":112,"./memoizeStringOnly":121}],5:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CallbackRegistry + * @typechecks static-only + */ + +"use strict"; + +var listenerBank = {}; + +/** + * Stores "listeners" by `registrationName`/`id`. There should be at most one + * "listener" per `registrationName`/`id` in the `listenerBank`. + * + * Access listeners via `listenerBank[registrationName][id]`. + * + * @class CallbackRegistry + * @internal + */ +var CallbackRegistry = { + + /** + * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent. + * + * @param {string} id ID of the DOM element. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @param {?function} listener The callback to store. + */ + putListener: function(id, registrationName, listener) { + var bankForRegistrationName = + listenerBank[registrationName] || (listenerBank[registrationName] = {}); + bankForRegistrationName[id] = listener; + }, + + /** + * @param {string} id ID of the DOM element. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @return {?function} The stored callback. + */ + getListener: function(id, registrationName) { + var bankForRegistrationName = listenerBank[registrationName]; + return bankForRegistrationName && bankForRegistrationName[id]; + }, + + /** + * Deletes a listener from the registration bank. + * + * @param {string} id ID of the DOM element. + * @param {string} registrationName Name of listener (e.g. `onClick`). + */ + deleteListener: function(id, registrationName) { + var bankForRegistrationName = listenerBank[registrationName]; + if (bankForRegistrationName) { + delete bankForRegistrationName[id]; + } + }, + + /** + * Deletes all listeners for the DOM element with the supplied ID. + * + * @param {string} id ID of the DOM element. + */ + deleteAllListeners: function(id) { + for (var registrationName in listenerBank) { + delete listenerBank[registrationName][id]; + } + }, + + /** + * This is needed for tests only. Do not use! + */ + __purge: function() { + listenerBank = {}; + } + +}; + +module.exports = CallbackRegistry; + +},{}],6:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule ChangeEventPlugin + */ + +"use strict"; + +var EventConstants = require("./EventConstants"); +var EventPluginHub = require("./EventPluginHub"); +var EventPropagators = require("./EventPropagators"); +var ExecutionEnvironment = require("./ExecutionEnvironment"); +var SyntheticEvent = require("./SyntheticEvent"); + +var isEventSupported = require("./isEventSupported"); +var isTextInputElement = require("./isTextInputElement"); +var keyOf = require("./keyOf"); + +var topLevelTypes = EventConstants.topLevelTypes; + +var eventTypes = { + change: { + phasedRegistrationNames: { + bubbled: keyOf({onChange: null}), + captured: keyOf({onChangeCapture: null}) + } + } +}; + +/** + * For IE shims + */ +var activeElement = null; +var activeElementID = null; +var activeElementValue = null; +var activeElementValueProp = null; + +/** + * SECTION: handle `change` event + */ +function shouldUseChangeEvent(elem) { + return ( + elem.nodeName === 'SELECT' || + (elem.nodeName === 'INPUT' && elem.type === 'file') + ); +} + +var doesChangeEventBubble = false; +if (ExecutionEnvironment.canUseDOM) { + // See `handleChange` comment below + doesChangeEventBubble = isEventSupported('change') && ( + !('documentMode' in document) || document.documentMode > 8 + ); +} + +function manualDispatchChangeEvent(nativeEvent) { + var event = SyntheticEvent.getPooled( + eventTypes.change, + activeElementID, + nativeEvent + ); + EventPropagators.accumulateTwoPhaseDispatches(event); + + // If change bubbled, we'd just bind to it like all the other events + // and have it go through ReactEventTopLevelCallback. Since it doesn't, we + // manually listen for the change event and so we have to enqueue and + // process the abstract event manually. + EventPluginHub.enqueueEvents(event); + EventPluginHub.processEventQueue(); +} + +function startWatchingForChangeEventIE8(target, targetID) { + activeElement = target; + activeElementID = targetID; + activeElement.attachEvent('onchange', manualDispatchChangeEvent); +} + +function stopWatchingForChangeEventIE8() { + if (!activeElement) { + return; + } + activeElement.detachEvent('onchange', manualDispatchChangeEvent); + activeElement = null; + activeElementID = null; +} + +function getTargetIDForChangeEvent( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topChange) { + return topLevelTargetID; + } +} +function handleEventsForChangeEventIE8( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topFocus) { + // stopWatching() should be a noop here but we call it just in case we + // missed a blur event somehow. + stopWatchingForChangeEventIE8(); + startWatchingForChangeEventIE8(topLevelTarget, topLevelTargetID); + } else if (topLevelType === topLevelTypes.topBlur) { + stopWatchingForChangeEventIE8(); + } +} + + +/** + * SECTION: handle `input` event + */ +var isInputEventSupported = false; +if (ExecutionEnvironment.canUseDOM) { + // IE9 claims to support the input event but fails to trigger it when + // deleting text, so we ignore its input events + isInputEventSupported = isEventSupported('input') && ( + !('documentMode' in document) || document.documentMode > 9 + ); +} + +/** + * (For old IE.) Replacement getter/setter for the `value` property that gets + * set on the active element. + */ +var newValueProp = { + get: function() { + return activeElementValueProp.get.call(this); + }, + set: function(val) { + // Cast to a string so we can do equality checks. + activeElementValue = '' + val; + activeElementValueProp.set.call(this, val); + } +}; + +/** + * (For old IE.) Starts tracking propertychange events on the passed-in element + * and override the value property so that we can distinguish user events from + * value changes in JS. + */ +function startWatchingForValueChange(target, targetID) { + activeElement = target; + activeElementID = targetID; + activeElementValue = target.value; + activeElementValueProp = Object.getOwnPropertyDescriptor( + target.constructor.prototype, + 'value' + ); + + Object.defineProperty(activeElement, 'value', newValueProp); + activeElement.attachEvent('onpropertychange', handlePropertyChange); +} + +/** + * (For old IE.) Removes the event listeners from the currently-tracked element, + * if any exists. + */ +function stopWatchingForValueChange() { + if (!activeElement) { + return; + } + + // delete restores the original property definition + delete activeElement.value; + activeElement.detachEvent('onpropertychange', handlePropertyChange); + + activeElement = null; + activeElementID = null; + activeElementValue = null; + activeElementValueProp = null; +} + +/** + * (For old IE.) Handles a propertychange event, sending a `change` event if + * the value of the active element has changed. + */ +function handlePropertyChange(nativeEvent) { + if (nativeEvent.propertyName !== 'value') { + return; + } + var value = nativeEvent.srcElement.value; + if (value === activeElementValue) { + return; + } + activeElementValue = value; + + manualDispatchChangeEvent(nativeEvent); +} + +/** + * If a `change` event should be fired, returns the target's ID. + */ +function getTargetIDForInputEvent( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topInput) { + // In modern browsers (i.e., not IE8 or IE9), the input event is exactly + // what we want so fall through here and trigger an abstract event + return topLevelTargetID; + } +} + +// For IE8 and IE9. +function handleEventsForInputEventIE( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topFocus) { + // In IE8, we can capture almost all .value changes by adding a + // propertychange handler and looking for events with propertyName + // equal to 'value' + // In IE9, propertychange fires for most input events but is buggy and + // doesn't fire when text is deleted, but conveniently, selectionchange + // appears to fire in all of the remaining cases so we catch those and + // forward the event if the value has changed + // In either case, we don't want to call the event handler if the value + // is changed from JS so we redefine a setter for `.value` that updates + // our activeElementValue variable, allowing us to ignore those changes + // + // stopWatching() should be a noop here but we call it just in case we + // missed a blur event somehow. + stopWatchingForValueChange(); + startWatchingForValueChange(topLevelTarget, topLevelTargetID); + } else if (topLevelType === topLevelTypes.topBlur) { + stopWatchingForValueChange(); + } +} + +// For IE8 and IE9. +function getTargetIDForInputEventIE( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topSelectionChange || + topLevelType === topLevelTypes.topKeyUp || + topLevelType === topLevelTypes.topKeyDown) { + // On the selectionchange event, the target is just document which isn't + // helpful for us so just check activeElement instead. + // + // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire + // propertychange on the first input event after setting `value` from a + // script and fires only keydown, keypress, keyup. Catching keyup usually + // gets it and catching keydown lets us fire an event for the first + // keystroke if user does a key repeat (it'll be a little delayed: right + // before the second keystroke). Other input methods (e.g., paste) seem to + // fire selectionchange normally. + if (activeElement && activeElement.value !== activeElementValue) { + activeElementValue = activeElement.value; + return activeElementID; + } + } +} + + +/** + * SECTION: handle `click` event + */ +function shouldUseClickEvent(elem) { + // Use the `click` event to detect changes to checkbox and radio inputs. + // This approach works across all browsers, whereas `change` does not fire + // until `blur` in IE8. + return ( + elem.nodeName === 'INPUT' && + (elem.type === 'checkbox' || elem.type === 'radio') + ); +} + +function getTargetIDForClickEvent( + topLevelType, + topLevelTarget, + topLevelTargetID) { + if (topLevelType === topLevelTypes.topClick) { + return topLevelTargetID; + } +} + +/** + * This plugin creates an `onChange` event that normalizes change events + * across form elements. This event fires at a time when it's possible to + * change the element's value without seeing a flicker. + * + * Supported elements are: + * - input (see `isTextInputElement`) + * - textarea + * - select + */ +var ChangeEventPlugin = { + + eventTypes: eventTypes, + + /** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of synthetic events. + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + + var getTargetIDFunc, handleEventFunc; + if (shouldUseChangeEvent(topLevelTarget)) { + if (doesChangeEventBubble) { + getTargetIDFunc = getTargetIDForChangeEvent; + } else { + handleEventFunc = handleEventsForChangeEventIE8; + } + } else if (isTextInputElement(topLevelTarget)) { + if (isInputEventSupported) { + getTargetIDFunc = getTargetIDForInputEvent; + } else { + getTargetIDFunc = getTargetIDForInputEventIE; + handleEventFunc = handleEventsForInputEventIE; + } + } else if (shouldUseClickEvent(topLevelTarget)) { + getTargetIDFunc = getTargetIDForClickEvent; + } + + if (getTargetIDFunc) { + var targetID = getTargetIDFunc( + topLevelType, + topLevelTarget, + topLevelTargetID + ); + if (targetID) { + var event = SyntheticEvent.getPooled( + eventTypes.change, + targetID, + nativeEvent + ); + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; + } + } + + if (handleEventFunc) { + handleEventFunc( + topLevelType, + topLevelTarget, + topLevelTargetID + ); + } + } + +}; + +module.exports = ChangeEventPlugin; + +},{"./EventConstants":15,"./EventPluginHub":17,"./EventPropagators":20,"./ExecutionEnvironment":21,"./SyntheticEvent":80,"./isEventSupported":114,"./isTextInputElement":116,"./keyOf":120}],7:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule CompositionEventPlugin + * @typechecks static-only + */ + +"use strict"; + +var EventConstants = require("./EventConstants"); +var EventPropagators = require("./EventPropagators"); +var ExecutionEnvironment = require("./ExecutionEnvironment"); +var ReactInputSelection = require("./ReactInputSelection"); +var SyntheticCompositionEvent = require("./SyntheticCompositionEvent"); + +var getTextContentAccessor = require("./getTextContentAccessor"); +var keyOf = require("./keyOf"); + +var END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space +var START_KEYCODE = 229; + +var useCompositionEvent = ExecutionEnvironment.canUseDOM && + 'CompositionEvent' in window; +var topLevelTypes = EventConstants.topLevelTypes; +var currentComposition = null; + +// Events and their corresponding property names. +var eventTypes = { + compositionEnd: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionEnd: null}), + captured: keyOf({onCompositionEndCapture: null}) + } + }, + compositionStart: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionStart: null}), + captured: keyOf({onCompositionStartCapture: null}) + } + }, + compositionUpdate: { + phasedRegistrationNames: { + bubbled: keyOf({onCompositionUpdate: null}), + captured: keyOf({onCompositionUpdateCapture: null}) + } + } +}; + +/** + * Translate native top level events into event types. + * + * @param {string} topLevelType + * @return {object} + */ +function getCompositionEventType(topLevelType) { + switch (topLevelType) { + case topLevelTypes.topCompositionStart: + return eventTypes.compositionStart; + case topLevelTypes.topCompositionEnd: + return eventTypes.compositionEnd; + case topLevelTypes.topCompositionUpdate: + return eventTypes.compositionUpdate; + } +} + +/** + * Does our fallback best-guess model think this event signifies that + * composition has begun? + * + * @param {string} topLevelType + * @param {object} nativeEvent + * @return {boolean} + */ +function isFallbackStart(topLevelType, nativeEvent) { + return ( + topLevelType === topLevelTypes.topKeyDown && + nativeEvent.keyCode === START_KEYCODE + ); +} + +/** + * Does our fallback mode think that this event is the end of composition? + * + * @param {string} topLevelType + * @param {object} nativeEvent + * @return {boolean} + */ +function isFallbackEnd(topLevelType, nativeEvent) { + switch (topLevelType) { + case topLevelTypes.topKeyUp: + // Command keys insert or clear IME input. + return (END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1); + case topLevelTypes.topKeyDown: + // Expect IME keyCode on each keydown. If we get any other + // code we must have exited earlier. + return (nativeEvent.keyCode !== START_KEYCODE); + case topLevelTypes.topKeyPress: + case topLevelTypes.topMouseDown: + case topLevelTypes.topBlur: + // Events are not possible without cancelling IME. + return true; + default: + return false; + } +} + +/** + * Helper class stores information about selection and document state + * so we can figure out what changed at a later date. + * + * @param {DOMEventTarget} root + */ +function FallbackCompositionState(root) { + this.root = root; + this.startSelection = ReactInputSelection.getSelection(root); + this.startValue = this.getText(); +} + +/** + * Get current text of input. + * + * @return {string} + */ +FallbackCompositionState.prototype.getText = function() { + return this.root.value || this.root[getTextContentAccessor()]; +}; + +/** + * Text that has changed since the start of composition. + * + * @return {string} + */ +FallbackCompositionState.prototype.getData = function() { + var endValue = this.getText(); + var prefixLength = this.startSelection.start; + var suffixLength = this.startValue.length - this.startSelection.end; + + return endValue.substr( + prefixLength, + endValue.length - suffixLength - prefixLength + ); +}; + +/** + * This plugin creates `onCompositionStart`, `onCompositionUpdate` and + * `onCompositionEnd` events on inputs, textareas and contentEditable + * nodes. + */ +var CompositionEventPlugin = { + + eventTypes: eventTypes, + + /** + * @param {string} topLevelType Record from `EventConstants`. + * @param {DOMEventTarget} topLevelTarget The listening component root node. + * @param {string} topLevelTargetID ID of `topLevelTarget`. + * @param {object} nativeEvent Native browser event. + * @return {*} An accumulation of synthetic events. + * @see {EventPluginHub.extractEvents} + */ + extractEvents: function( + topLevelType, + topLevelTarget, + topLevelTargetID, + nativeEvent) { + + var eventType; + var data; + + if (useCompositionEvent) { + eventType = getCompositionEventType(topLevelType); + } else if (!currentComposition) { + if (isFallbackStart(topLevelType, nativeEvent)) { + eventType = eventTypes.start; + currentComposition = new FallbackCompositionState(topLevelTarget); + } + } else if (isFallbackEnd(topLevelType, nativeEvent)) { + eventType = eventTypes.compositionEnd; + data = currentComposition.getData(); + currentComposition = null; + } + + if (eventType) { + var event = SyntheticCompositionEvent.getPooled( + eventType, + topLevelTargetID, + nativeEvent + ); + if (data) { + // Inject data generated from fallback path into the synthetic event. + // This matches the property of native CompositionEventInterface. + event.data = data; + } + EventPropagators.accumulateTwoPhaseDispatches(event); + return event; + } + } +}; + +module.exports = CompositionEventPlugin; + +},{"./EventConstants":15,"./EventPropagators":20,"./ExecutionEnvironment":21,"./ReactInputSelection":51,"./SyntheticCompositionEvent":79,"./getTextContentAccessor":110,"./keyOf":120}],8:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule DOMChildrenOperations + * @typechecks static-only + */ + +"use strict"; + +var Danger = require("./Danger"); +var ReactMultiChildUpdateTypes = require("./ReactMultiChildUpdateTypes"); + +var getTextContentAccessor = require("./getTextContentAccessor"); + +/** + * The DOM property to use when setting text content. + * + * @type {string} + * @private + */ +var textContentAccessor = getTextContentAccessor() || 'NA'; + +/** + * Inserts `childNode` as a child of `parentNode` at the `index`. + * + * @param {DOMElement} parentNode Parent node in which to insert. + * @param {DOMElement} childNode Child node to insert. + * @param {number} index Index at which to insert the child. + * @internal + */ +function insertChildAt(parentNode, childNode, index) { + var childNodes = parentNode.childNodes; + if (childNodes[index] === childNode) { + return; + } + // If `childNode` is already a child of `parentNode`, remove it so that + // computing `childNodes[index]` takes into account the removal. + if (childNode.parentNode === parentNode) { + parentNode.removeChild(childNode); + } + if (index >= childNodes.length) { + parentNode.appendChild(childNode); + } else { + parentNode.insertBefore(childNode, childNodes[index]); + } +} + +/** + * Operations for updating with DOM children. + */ +var DOMChildrenOperations = { + + dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, + + /** + * Updates a component's children by processing a series of updates. The + * update configurations are each expected to have a `parentNode` property. + * + * @param {array} updates List of update configurations. + * @param {array} markupList List of markup strings. + * @internal + */ + processUpdates: function(updates, markupList) { + var update; + // Mapping from parent IDs to initial child orderings. + var initialChildren = null; + // List of children that will be moved or removed. + var updatedChildren = null; + + for (var i = 0; update = updates[i]; i++) { + if (update.type === ReactMultiChildUpdateTypes.MOVE_EXISTING || + update.type === ReactMultiChildUpdateTypes.REMOVE_NODE) { + var updatedIndex = update.fromIndex; + var updatedChild = update.parentNode.childNodes[updatedIndex]; + var parentID = update.parentID; + + initialChildren = initialChildren || {}; + initialChildren[parentID] = initialChildren[parentID] || []; + initialChildren[parentID][updatedIndex] = updatedChild; + + updatedChildren = updatedChildren || []; + updatedChildren.push(updatedChild); + } + } + + var renderedMarkup = Danger.dangerouslyRenderMarkup(markupList); + + // Remove updated children first so that `toIndex` is consistent. + if (updatedChildren) { + for (var j = 0; j < updatedChildren.length; j++) { + updatedChildren[j].parentNode.removeChild(updatedChildren[j]); + } + } + + for (var k = 0; update = updates[k]; k++) { + switch (update.type) { + case ReactMultiChildUpdateTypes.INSERT_MARKUP: + insertChildAt( + update.parentNode, + renderedMarkup[update.markupIndex], + update.toIndex + ); + break; + case ReactMultiChildUpdateTypes.MOVE_EXISTING: + insertChildAt( + update.parentNode, + initialChildren[update.parentID][update.fromIndex], + update.toIndex + ); + break; + case ReactMultiChildUpdateTypes.TEXT_CONTENT: + update.parentNode[textContentAccessor] = update.textContent; + break; + case ReactMultiChildUpdateTypes.REMOVE_NODE: + // Already removed by the for-loop above. + break; + } + } + } + +}; + +module.exports = DOMChildrenOperations; + +},{"./Danger":11,"./ReactMultiChildUpdateTypes":58,"./getTextContentAccessor":110}],9:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule DOMProperty + * @typechecks static-only + */ + +/*jslint bitwise: true */ + +"use strict"; + +var invariant = require("./invariant"); + +var DOMPropertyInjection = { + /** + * Mapping from normalized, camelcased property names to a configuration that + * specifies how the associated DOM property should be accessed or rendered. + */ + MUST_USE_ATTRIBUTE: 0x1, + MUST_USE_PROPERTY: 0x2, + HAS_SIDE_EFFECTS: 0x4, + HAS_BOOLEAN_VALUE: 0x8, + HAS_POSITIVE_NUMERIC_VALUE: 0x10, + + /** + * Inject some specialized knowledge about the DOM. This takes a config object + * with the following properties: + * + * isCustomAttribute: function that given an attribute name will return true + * if it can be inserted into the DOM verbatim. Useful for data-* or aria-* + * attributes where it's impossible to enumerate all of the possible + * attribute names, + * + * Properties: object mapping DOM property name to one of the + * DOMPropertyInjection constants or null. If your attribute isn't in here, + * it won't get written to the DOM. + * + * DOMAttributeNames: object mapping React attribute name to the DOM + * attribute name. Attribute names not specified use the **lowercase** + * normalized name. + * + * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. + * Property names not specified use the normalized name. + * + * DOMMutationMethods: Properties that require special mutation methods. If + * `value` is undefined, the mutation method should unset the property. + * + * @param {object} domPropertyConfig the config as described above. + */ + injectDOMPropertyConfig: function(domPropertyConfig) { + var Properties = domPropertyConfig.Properties || {}; + var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; + var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {}; + var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; + + if (domPropertyConfig.isCustomAttribute) { + DOMProperty._isCustomAttributeFunctions.push( + domPropertyConfig.isCustomAttribute + ); + } + + for (var propName in Properties) { + ("production" !== "development" ? invariant( + !DOMProperty.isStandardName[propName], + 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property ' + + '\'%s\' which has already been injected. You may be accidentally ' + + 'injecting the same DOM property config twice, or you may be ' + + 'injecting two configs that have conflicting property names.', + propName + ) : invariant(!DOMProperty.isStandardName[propName])); + + DOMProperty.isStandardName[propName] = true; + + var lowerCased = propName.toLowerCase(); + DOMProperty.getPossibleStandardName[lowerCased] = propName; + + var attributeName = DOMAttributeNames[propName]; + if (attributeName) { + DOMProperty.getPossibleStandardName[attributeName] = propName; + } + + DOMProperty.getAttributeName[propName] = attributeName || lowerCased; + + DOMProperty.getPropertyName[propName] = + DOMPropertyNames[propName] || propName; + + var mutationMethod = DOMMutationMethods[propName]; + if (mutationMethod) { + DOMProperty.getMutationMethod[propName] = mutationMethod; + } + + var propConfig = Properties[propName]; + DOMProperty.mustUseAttribute[propName] = + propConfig & DOMPropertyInjection.MUST_USE_ATTRIBUTE; + DOMProperty.mustUseProperty[propName] = + propConfig & DOMPropertyInjection.MUST_USE_PROPERTY; + DOMProperty.hasSideEffects[propName] = + propConfig & DOMPropertyInjection.HAS_SIDE_EFFECTS; + DOMProperty.hasBooleanValue[propName] = + propConfig & DOMPropertyInjection.HAS_BOOLEAN_VALUE; + DOMProperty.hasPositiveNumericValue[propName] = + propConfig & DOMPropertyInjection.HAS_POSITIVE_NUMERIC_VALUE; + + ("production" !== "development" ? invariant( + !DOMProperty.mustUseAttribute[propName] || + !DOMProperty.mustUseProperty[propName], + 'DOMProperty: Cannot require using both attribute and property: %s', + propName + ) : invariant(!DOMProperty.mustUseAttribute[propName] || + !DOMProperty.mustUseProperty[propName])); + ("production" !== "development" ? invariant( + DOMProperty.mustUseProperty[propName] || + !DOMProperty.hasSideEffects[propName], + 'DOMProperty: Properties that have side effects must use property: %s', + propName + ) : invariant(DOMProperty.mustUseProperty[propName] || + !DOMProperty.hasSideEffects[propName])); + ("production" !== "development" ? invariant( + !DOMProperty.hasBooleanValue[propName] || + !DOMProperty.hasPositiveNumericValue[propName], + 'DOMProperty: Cannot have both boolean and positive numeric value: %s', + propName + ) : invariant(!DOMProperty.hasBooleanValue[propName] || + !DOMProperty.hasPositiveNumericValue[propName])); + } + } +}; +var defaultValueCache = {}; + +/** + * DOMProperty exports lookup objects that can be used like functions: + * + * > DOMProperty.isValid['id'] + * true + * > DOMProperty.isValid['foobar'] + * undefined + * + * Although this may be confusing, it performs better in general. + * + * @see http://jsperf.com/key-exists + * @see http://jsperf.com/key-missing + */ +var DOMProperty = { + + /** + * Checks whether a property name is a standard property. + * @type {Object} + */ + isStandardName: {}, + + /** + * Mapping from lowercase property names to the properly cased version, used + * to warn in the case of missing properties. + * @type {Object} + */ + getPossibleStandardName: {}, + + /** + * Mapping from normalized names to attribute names that differ. Attribute + * names are used when rendering markup or with `*Attribute()`. + * @type {Object} + */ + getAttributeName: {}, + + /** + * Mapping from normalized names to properties on DOM node instances. + * (This includes properties that mutate due to external factors.) + * @type {Object} + */ + getPropertyName: {}, + + /** + * Mapping from normalized names to mutation methods. This will only exist if + * mutation cannot be set simply by the property or `setAttribute()`. + * @type {Object} + */ + getMutationMethod: {}, + + /** + * Whether the property must be accessed and mutated as an object property. + * @type {Object} + */ + mustUseAttribute: {}, + + /** + * Whether the property must be accessed and mutated using `*Attribute()`. + * (This includes anything that fails ` in `.) + * @type {Object} + */ + mustUseProperty: {}, + + /** + * Whether or not setting a value causes side effects such as triggering + * resources to be loaded or text selection changes. We must ensure that + * the value is only set if it has changed. + * @type {Object} + */ + hasSideEffects: {}, + + /** + * Whether the property should be removed when set to a falsey value. + * @type {Object} + */ + hasBooleanValue: {}, + + /** + * Whether the property must be positive numeric or parse as a positive + * numeric and should be removed when set to a falsey value. + * @type {Object} + */ + hasPositiveNumericValue: {}, + + /** + * All of the isCustomAttribute() functions that have been injected. + */ + _isCustomAttributeFunctions: [], + + /** + * Checks whether a property name is a custom attribute. + * @method + */ + isCustomAttribute: function(attributeName) { + return DOMProperty._isCustomAttributeFunctions.some( + function(isCustomAttributeFn) { + return isCustomAttributeFn.call(null, attributeName); + } + ); + }, + + /** + * Returns the default property value for a DOM property (i.e., not an + * attribute). Most default values are '' or false, but not all. Worse yet, + * some (in particular, `type`) vary depending on the type of element. + * + * TODO: Is it better to grab all the possible properties when creating an + * element to avoid having to create the same element twice? + */ + getDefaultValueForProperty: function(nodeName, prop) { + var nodeDefaults = defaultValueCache[nodeName]; + var testElement; + if (!nodeDefaults) { + defaultValueCache[nodeName] = nodeDefaults = {}; + } + if (!(prop in nodeDefaults)) { + testElement = document.createElement(nodeName); + nodeDefaults[prop] = testElement[prop]; + } + return nodeDefaults[prop]; + }, + + injection: DOMPropertyInjection +}; + +module.exports = DOMProperty; + +},{"./invariant":113}],10:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule DOMPropertyOperations + * @typechecks static-only + */ + +"use strict"; + +var DOMProperty = require("./DOMProperty"); + +var escapeTextForBrowser = require("./escapeTextForBrowser"); +var memoizeStringOnly = require("./memoizeStringOnly"); + +function shouldIgnoreValue(name, value) { + return value == null || + DOMProperty.hasBooleanValue[name] && !value || + DOMProperty.hasPositiveNumericValue[name] && (isNaN(value) || value < 1); +} + +var processAttributeNameAndPrefix = memoizeStringOnly(function(name) { + return escapeTextForBrowser(name) + '="'; +}); + +if ("production" !== "development") { + var reactProps = { + children: true, + dangerouslySetInnerHTML: true, + key: true, + ref: true + }; + var warnedProperties = {}; + + var warnUnknownProperty = function(name) { + if (reactProps[name] || warnedProperties[name]) { + return; + } + + warnedProperties[name] = true; + var lowerCasedName = name.toLowerCase(); + + // data-* attributes should be lowercase; suggest the lowercase version + var standardName = DOMProperty.isCustomAttribute(lowerCasedName) ? + lowerCasedName : DOMProperty.getPossibleStandardName[lowerCasedName]; + + // For now, only warn when we have a suggested correction. This prevents + // logging too much when using transferPropsTo. + if (standardName != null) { + console.warn( + 'Unknown DOM property ' + name + '. Did you mean ' + standardName + '?' + ); + } + + }; +} + +/** + * Operations for dealing with DOM properties. + */ +var DOMPropertyOperations = { + + /** + * Creates markup for a property. + * + * @param {string} name + * @param {*} value + * @return {?string} Markup string, or null if the property was invalid. + */ + createMarkupForProperty: function(name, value) { + if (DOMProperty.isStandardName[name]) { + if (shouldIgnoreValue(name, value)) { + return ''; + } + var attributeName = DOMProperty.getAttributeName[name]; + return processAttributeNameAndPrefix(attributeName) + + escapeTextForBrowser(value) + '"'; + } else if (DOMProperty.isCustomAttribute(name)) { + if (value == null) { + return ''; + } + return processAttributeNameAndPrefix(name) + + escapeTextForBrowser(value) + '"'; + } else if ("production" !== "development") { + warnUnknownProperty(name); + } + return null; + }, + + /** + * Sets the value for a property on a node. + * + * @param {DOMElement} node + * @param {string} name + * @param {*} value + */ + setValueForProperty: function(node, name, value) { + if (DOMProperty.isStandardName[name]) { + var mutationMethod = DOMProperty.getMutationMethod[name]; + if (mutationMethod) { + mutationMethod(node, value); + } else if (shouldIgnoreValue(name, value)) { + this.deleteValueForProperty(node, name); + } else if (DOMProperty.mustUseAttribute[name]) { + node.setAttribute(DOMProperty.getAttributeName[name], '' + value); + } else { + var propName = DOMProperty.getPropertyName[name]; + if (!DOMProperty.hasSideEffects[name] || node[propName] !== value) { + node[propName] = value; + } + } + } else if (DOMProperty.isCustomAttribute(name)) { + if (value == null) { + node.removeAttribute(DOMProperty.getAttributeName[name]); + } else { + node.setAttribute(name, '' + value); + } + } else if ("production" !== "development") { + warnUnknownProperty(name); + } + }, + + /** + * Deletes the value for a property on a node. + * + * @param {DOMElement} node + * @param {string} name + */ + deleteValueForProperty: function(node, name) { + if (DOMProperty.isStandardName[name]) { + var mutationMethod = DOMProperty.getMutationMethod[name]; + if (mutationMethod) { + mutationMethod(node, undefined); + } else if (DOMProperty.mustUseAttribute[name]) { + node.removeAttribute(DOMProperty.getAttributeName[name]); + } else { + var propName = DOMProperty.getPropertyName[name]; + var defaultValue = DOMProperty.getDefaultValueForProperty( + node.nodeName, + name + ); + if (!DOMProperty.hasSideEffects[name] || + node[propName] !== defaultValue) { + node[propName] = defaultValue; + } + } + } else if (DOMProperty.isCustomAttribute(name)) { + node.removeAttribute(name); + } else if ("production" !== "development") { + warnUnknownProperty(name); + } + } + +}; + +module.exports = DOMPropertyOperations; + +},{"./DOMProperty":9,"./escapeTextForBrowser":99,"./memoizeStringOnly":121}],11:[function(require,module,exports){ +/** + * Copyright 2013 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule Danger + * @typechecks static-only + */ + +/*jslint evil: true, sub: true */ + +"use strict"; + +var ExecutionEnvironment = require("./ExecutionEnvironment"); + +var createNodesFromMarkup = require("./createNodesFromMarkup"); +var emptyFunction = require("./emptyFunction"); +var getMarkupWrap = require("./getMarkupWrap"); +var invariant = require("./invariant"); +var mutateHTMLNodeWithMarkup = require("./mutateHTMLNodeWithMarkup"); + +var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/; +var RESULT_INDEX_ATTR = 'data-danger-index'; + +/** + * Extracts the `nodeName` from a string of markup. + * + * NOTE: Extracting the `nodeName` does not require a regular expression match + * because we make assumptions about React-generated markup (i.e. there are no + * spaces surrounding the opening tag and there is at least one attribute). + * + * @param {string} markup String of markup. + * @return {string} Node name of the supplied markup. + * @see http://jsperf.com/extract-nodename + */ +function getNodeName(markup) { + return markup.substring(1, markup.indexOf(' ')); +} + +var Danger = { + + /** + * Renders markup into an array of nodes. The markup is expected to render + * into a list of root nodes. Also, the length of `resultList` and + * `markupList` should be the same. + * + * @param {array} markupList List of markup strings to render. + * @return {array} List of rendered nodes. + * @internal + */ + dangerouslyRenderMarkup: function(markupList) { + ("production" !== "development" ? invariant( + ExecutionEnvironment.canUseDOM, + 'dangerouslyRenderMarkup(...): Cannot render markup in a Worker ' + + 'thread. This is likely a bug in the framework. Please report ' + + 'immediately.' + ) : invariant(ExecutionEnvironment.canUseDOM)); + var nodeName; + var markupByNodeName = {}; + // Group markup by `nodeName` if a wrap is necessary, else by '*'. + for (var i = 0; i < markupList.length; i++) { + ("production" !== "development" ? invariant( + markupList[i], + 'dangerouslyRenderMarkup(...): Missing markup.' + ) : invariant(markupList[i])); + nodeName = getNodeName(markupList[i]); + nodeName = getMarkupWrap(nodeName) ? nodeName : '*'; + markupByNodeName[nodeName] = markupByNodeName[nodeName] || []; + markupByNodeName[nodeName][i] = markupList[i]; + } + var resultList = []; + var resultListAssignmentCount = 0; + for (nodeName in markupByNodeName) { + if (!markupByNodeName.hasOwnProperty(nodeName)) { + continue; + } + var markupListByNodeName = markupByNodeName[nodeName]; + + // This for-in loop skips the holes of the sparse array. The order of + // iteration should follow the order of assignment, which happens to match + // numerical index order, but we don't rely on that. + for (var resultIndex in markupListByNodeName) { + if (markupListByNodeName.hasOwnProperty(resultIndex)) { + var markup = markupListByNodeName[resultIndex]; + + // Push the requested markup with an additional RESULT_INDEX_ATTR + // attribute. If the markup does not start with a < character, it + // will be discarded below (with an appropriate console.error). + markupListByNodeName[resultIndex] = markup.replace( + OPEN_TAG_NAME_EXP, + // This index will be parsed back out below. + '$1 ' + RESULT_INDEX_ATTR + '="' + resultIndex + '" ' + ); + } + } + + // Render each group of markup with similar wrapping `nodeName`. + var renderNodes = createNodesFromMarkup( + markupListByNodeName.join(''), + emptyFunction // Do nothing special with