diff --git a/.gitignore b/.gitignore index bf0c3d8c..e8da2d46 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,6 @@ fixtures/typings-test/*.js # Directories needed for code coverage /coverage/ -# Keep legacy files out of the repo, this can be removed when we merge v3 into master +# Keep bundled code out of Git dist/ -src/utils/TestUtils.ts -src/xterm.js - -# Keep the demo builds out of Git demo/dist/ diff --git a/.travis.yml b/.travis.yml index 1878c3a4..c41ebf79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js os: - linux - - osx node_js: - 6 before_install: @@ -12,6 +11,7 @@ before_install: - npm install -g npm@5.1.0 env: matrix: + - NPM_COMMAND=tsc - NPM_COMMAND=lint - NPM_COMMAND=test notifications: diff --git a/AUTHORS b/AUTHORS index 1bd4c87d..47328bc9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,12 +1,15 @@ List of xterm.js contributors. Updated before every release. +Adrian Labbé Aleksandr Andrienko Aleksandr Andriienko Alessandro Nadalin Alexander Olsson Alexey Kontsevoy +Andres Mejia Anish Athalye Anthony Lapenna +Antonin Stefanutti Antonis Kalipetis Anton Skshidlevsky Anton Yurovskykh @@ -19,6 +22,7 @@ Bill Church Bob Reid bottleofwater Brian Mock +Bruno Ribeiro Bruno Ribeito Carson Anderson CHaBou @@ -26,6 +30,7 @@ Christian Budde Christensen Christof Marti Christopher Jeffrey coderaiser +Damien Tournoud Dan Brown Daniel Griffen Daniel Imms @@ -46,20 +51,25 @@ imoses InDieTasten irokas Jakob Gillich +Jan Kuri Jean Bruenn Jeremy Danyow +Jianhui Zhao Joao Moreno Joao Moreno Johannes Zellner Jon Masters Jörg Breitbart +Justin Luk Justin Mecham Kirill Merkushev +Krasimir Tsonev Luca Lucian Buzzo Lukas Drgon Maël Nison Marc Dumais +Markus F.X.J. Oberhumer Martin Chloride Martin Koppehel Martin Wang @@ -75,10 +85,12 @@ Paris Kasidiaris Peter Baumgarten Rick Baker runarberg +Saad Malik Samuel Williams Saswat Das Saul Costa Shuanglei Tao +sitzmar Steven Silvester stuicey t-amqi @@ -90,3 +102,4 @@ Tyler Jewell Vincent Woo yutaka YuviPanda +ZHAO Xudong diff --git a/Dockerfile b/Dockerfile index 22556f43..1c72e679 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6 +FROM node:8 MAINTAINER Paris Kasidiaris # Set the working directory diff --git a/Procfile b/Procfile index 063b78f4..44bf043c 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ web: npm start +webpack: npm run watch diff --git a/README.md b/README.md index 1719efb2..cdaf0d71 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ It enables applications to provide fully featured terminals to their users and c First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running: ``` -npm install +npm install xterm ``` To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `
` onto which xterm can attach itself. @@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. - [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. -- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible +- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. @@ -120,32 +120,72 @@ computational environment for Jupyter, supporting interactive data science and s - [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. - [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm. - [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters. +- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. +- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. +- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. +- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS +- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. +- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux +- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. ## Demo -### Linux or macOS +Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host. -First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run these commands: +Below you can find instructions on how to run the demo on different platforms. + +### SourceLair + +SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs). + +Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo. + +### Docker + +First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal: + +``` +docker-compose up +``` + +Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port. + +### Foreman (or other Procfile runner) + +First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal: ``` npm install -npm start +foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner ``` -Then open http://0.0.0.0:3000 in a web browser. +Then open http://0.0.0.0:3000 in a web browser to access the demo. + +### Linux or macOS + +First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal: + +``` +npm install +npm start # Run this in its own terminal +npm run watch # Run this in its own terminal +``` + +Then open http://0.0.0.0:3000 in a web browser to access the demo. ### Windows -First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands. +First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal: ``` npm install -npm start +npm start # Run this in its own terminal +npm run watch # Run this in its own terminal ``` -Then open http://127.0.0.1:3000 in a web browser. +Then open http://127.0.0.1:3000 in a web browser to access the demo. *Note: Do not use ConEmu, as it seems to break the demo for some reason.* @@ -161,10 +201,14 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. +### Code structure + +`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker. + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. +Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License) Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License) - Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) diff --git a/demo/index.html b/demo/index.html index 93399f9c..760d5990 100644 --- a/demo/index.html +++ b/demo/index.html @@ -23,6 +23,9 @@

+

+ +

+ + +
+
+

Accessibility

+

+ +

-

Attention: The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.

+

Attention: The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.

diff --git a/demo/main.js b/demo/main.js index 95b15a0d..7feb7939 100644 --- a/demo/main.js +++ b/demo/main.js @@ -27,28 +27,34 @@ var terminalContainer = document.getElementById('terminal-container'), optionElements = { cursorBlink: document.querySelector('#option-cursor-blink'), cursorStyle: document.querySelector('#option-cursor-style'), + macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'), scrollback: document.querySelector('#option-scrollback'), tabstopwidth: document.querySelector('#option-tabstopwidth'), - bellStyle: document.querySelector('#option-bell-style') + bellStyle: document.querySelector('#option-bell-style'), + screenReaderMode: document.querySelector('#option-screen-reader-mode') }, colsElement = document.getElementById('cols'), - rowsElement = document.getElementById('rows'); + rowsElement = document.getElementById('rows'), + paddingElement = document.getElementById('padding'); function setTerminalSize() { var cols = parseInt(colsElement.value, 10); var rows = parseInt(rowsElement.value, 10); - var viewportElement = document.querySelector('.xterm-viewport'); - var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth; - var width = (cols * term.renderer.dimensions.actualCellWidth + 20 /*room for scrollbar*/).toString() + 'px'; + var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px'; var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px'; - terminalContainer.style.width = width; terminalContainer.style.height = height; - term.resize(cols, rows); + term.fit(); +} + +function setPadding() { + term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; + term.fit(); } colsElement.addEventListener('change', setTerminalSize); rowsElement.addEventListener('change', setTerminalSize); +paddingElement.addEventListener('change', setPadding); actionElements.findNext.addEventListener('keypress', function (e) { if (e.key === "Enter") { @@ -72,12 +78,18 @@ optionElements.cursorStyle.addEventListener('change', function () { optionElements.bellStyle.addEventListener('change', function () { term.setOption('bellStyle', optionElements.bellStyle.value); }); +optionElements.macOptionIsMeta.addEventListener('change', function () { + term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked); +}); optionElements.scrollback.addEventListener('change', function () { term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); }); optionElements.tabstopwidth.addEventListener('change', function () { term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); }); +optionElements.screenReaderMode.addEventListener('change', function () { + term.setOption('screenReaderMode', optionElements.screenReaderMode.checked); +}); createTerminal(); @@ -87,9 +99,11 @@ function createTerminal() { terminalContainer.removeChild(terminalContainer.children[0]); } term = new Terminal({ + macOptionIsMeta: optionElements.macOptionIsMeta.enabled, cursorBlink: optionElements.cursorBlink.checked, scrollback: parseInt(optionElements.scrollback.value, 10), - tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10) + tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10), + screenReaderMode: optionElements.screenReaderMode.checked }); window.term = term; // Expose `term` to window for debugging purposes term.on('resize', function (size) { @@ -114,6 +128,7 @@ function createTerminal() { setTimeout(function () { colsElement.value = term.cols; rowsElement.value = term.rows; + paddingElement.value = 0; // Set terminal size again to set the specific dimensions on the demo setTerminalSize(); diff --git a/docker-compose.yml b/docker-compose.yml index 03e88fbc..effd975c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,3 +7,10 @@ services: - ./:/usr/src/app ports: - ${XTERMJS_PORT:3000}:3000 + command: ["npm", "start"] + + watch: + build: . + volumes: + - ./:/usr/src/app + command: ["npm", "run", "watch"] diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 5a5f6c50..13da6961 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -144,6 +144,11 @@ namespace methods_core { const r20: string = t.getOption('bellStyle'); const r21: boolean = t.getOption('enableBold'); const r22: number = t.getOption('letterSpacing'); + const r23: boolean = t.getOption('macOptionIsMeta'); + const r24: string = t.getOption('fontWeight'); + const r25: string = t.getOption('fontWeightBold'); + const r26: boolean = t.getOption('allowTransparency'); + const r27: boolean = t.getOption('rightClickSelectsWord'); } { const t: Terminal = new Terminal(); @@ -157,9 +162,14 @@ namespace methods_core { t.setOption('debug', true); t.setOption('disableStdin', true); t.setOption('enableBold', true); + t.setOption('fontWeight', 'normal'); + t.setOption('fontWeight', 'bold'); + t.setOption('fontWeightBold', 'normal'); + t.setOption('fontWeightBold', 'bold'); t.setOption('popOnBell', true); t.setOption('screenKeys', true); t.setOption('useFlowControl', true); + t.setOption('allowTransparency', true); t.setOption('visualBell', true); t.setOption('colors', ['a', 'b']); t.setOption('letterSpacing', 1); @@ -177,6 +187,8 @@ namespace methods_core { t.setOption('lineHeight', 1); t.setOption('fontFamily', 'foo'); t.setOption('theme', {background: '#ff0000'}); + t.setOption('macOptionIsMeta', true); + t.setOption('rightClickSelectsWord', false); } } namespace scrolling { diff --git a/gulpfile.js b/gulpfile.js index 49d67ed6..4353e46d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,11 +18,13 @@ const ts = require('gulp-typescript'); const util = require('gulp-util'); const webpack = require('webpack-stream'); -let buildDir = process.env.BUILD_DIR || 'build'; -let tsProject = ts.createProject('tsconfig.json'); -let srcDir = tsProject.config.compilerOptions.rootDir; +const buildDir = process.env.BUILD_DIR || 'build'; +const tsProject = ts.createProject('tsconfig.json'); +const srcDir = tsProject.config.compilerOptions.rootDir; let outDir = tsProject.config.compilerOptions.outDir; +const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem']; + // Under some environments like TravisCI, this comes out at absolute which can // break the build. This ensures that the outDir is absolute. if (path.normalize(outDir).indexOf(__dirname) !== 0) { @@ -45,15 +47,19 @@ gulp.task('tsc', function () { tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)) ); - let addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem']; let addonStreams = addons.map(function(addon) { fs.emptyDirSync(`${outDir}/addons/${addon}`); let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`); let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon()); - let tscAddon = tsResultAddon.js - .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) - .pipe(gulp.dest(`${outDir}/addons/${addon}`)); + let tscAddon = merge( + tsResultAddon.js + .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) + .pipe(gulp.dest(`${outDir}/addons/${addon}`)), + tsResultAddon.dts + .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) + .pipe(gulp.dest(`${outDir}/addons/${addon}`)) + ) return tscAddon; }); @@ -103,45 +109,29 @@ gulp.task('browserify', ['tsc'], function() { }); gulp.task('browserify-addons', ['tsc'], function() { - let searchOptions = { - basedir: `${buildDir}/addons/search`, - debug: true, - entries: [`${outDir}/addons/search/search.js`], - cache: {}, - packageCache: {} - }; - let searchBundle = browserify(searchOptions) - .external(path.join(outDir, 'Terminal.js')) - .bundle() - .pipe(source('./addons/search/search.js')) - .pipe(buffer()) - .pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''})) - .pipe(sourcemaps.write('./')) - .pipe(gulp.dest(buildDir)); + const bundles = addons.map((addon) => { + const addonOptions = { + basedir: `${buildDir}/addons/${addon}`, + debug: true, + entries: [`${outDir}/addons/${addon}/${addon}.js`], + standalone: addon, + cache: {}, + packageCache: {} + }; - let winptyCompatOptions = { - basedir: `${buildDir}/addons/winptyCompat`, - debug: true, - entries: [`${outDir}/addons/winptyCompat/winptyCompat.js`], - cache: {}, - packageCache: {} - }; - let winptyCompatBundle = browserify(winptyCompatOptions) - .external(path.join(outDir, 'Terminal.js')) - .bundle() - .pipe(source('./addons/winptyCompat/winptyCompat.js')) - .pipe(buffer()) - .pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''})) - .pipe(sourcemaps.write('./')) - .pipe(gulp.dest(buildDir)); + const addonBundle = browserify(addonOptions) + .external(path.join(outDir, 'Terminal.js')) + .bundle() + .pipe(source(`./addons/${addon}/${addon}.js`)) + .pipe(buffer()) + .pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''})) + .pipe(sourcemaps.write('./')) + .pipe(gulp.dest(buildDir)); - // Copy all add-ons from outDir to buildDir - let copyAddons = gulp.src([ - // Copy JS addons - `${outDir}/addons/**/*` - ]).pipe(gulp.dest(`${buildDir}/addons`)); + return addonBundle; + }); - return merge(searchBundle, winptyCompatBundle, copyAddons); + return merge(...bundles); }); gulp.task('instrument-test', function () { @@ -189,9 +179,11 @@ gulp.task('sorcery', ['browserify'], function () { }); gulp.task('sorcery-addons', ['browserify-addons'], function () { - var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`); - chain.apply(); - chain.writeSync(); + addons.forEach((addon) => { + const chain = sorcery.loadSync(`${buildDir}/addons/${addon}/${addon}.js`); + chain.apply(); + chain.writeSync(); + }) }); gulp.task('webpack', ['build'], function() { @@ -200,6 +192,10 @@ gulp.task('webpack', ['build'], function() { .pipe(gulp.dest('demo/dist/')); }); +gulp.task('watch', ['webpack'], () => { + gulp.watch(['./src/*', './src/**/*'], ['webpack']); +}); + /** * Submit coverage results to coveralls.io */ diff --git a/package-lock.json b/package-lock.json index c83a4d9a..e2478592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "xterm", - "version": "2.9.1", + "version": "3.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -56,6 +56,16 @@ "integrity": "sha512-bJgf+gJF+zohYkhOOdY7cXVKa0jMZGICqIcdPRb1SFyPHI4sKau0tXTRLmT2T4S5bjx6cY9AuoMkB5v+TIL4wA==", "dev": true }, + "JSONStream": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", + "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, "abab": { "version": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz", "integrity": "sha1-uB3l9ydOxOdW15fNg08wNkJyTl0=", @@ -67,6 +77,15 @@ "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", "dev": true }, + "acorn-dynamic-import": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", + "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "dev": true, + "requires": { + "acorn": "4.0.13" + } + }, "acorn-globals": { "version": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", @@ -82,6 +101,35 @@ } } }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, "amdefine": { "version": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", @@ -98,6 +146,40 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha1-VT3Lj5HjyImEXf26NMd3IbkLnXo=", + "dev": true, + "requires": { + "micromatch": "2.3.11", + "normalize-path": "2.1.1" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=", + "dev": true + }, "array-differ": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", @@ -145,6 +227,12 @@ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", "dev": true }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, "asn1": { "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", @@ -184,6 +272,21 @@ "acorn": "4.0.13" } }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "dev": true, + "requires": { + "lodash": "4.17.4" + } + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, "asynckit": { "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", @@ -199,10 +302,90 @@ "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, "base64-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", + "integrity": "sha1-qRlH2h9KUW6jjltOwOw3c2deCIY=", "dev": true }, "bcrypt-pbkdf": { @@ -220,6 +403,18 @@ "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", "dev": true }, + "big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true + }, + "binary-extensions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "dev": true + }, "bn.js": { "version": "4.11.7", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.7.tgz", @@ -234,6 +429,27 @@ "hoek": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" } }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -246,9 +462,9 @@ "integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=", "dev": true, "requires": { + "JSONStream": "1.3.1", "combine-source-map": "0.7.2", "defined": "1.0.0", - "JSONStream": "1.3.1", "through2": "2.0.3", "umd": "3.0.1" } @@ -276,6 +492,7 @@ "integrity": "sha1-tanJAgJD8McORnW+yCI7xifkFc4=", "dev": true, "requires": { + "JSONStream": "1.3.1", "assert": "1.4.1", "browser-pack": "6.0.2", "browser-resolve": "1.11.2", @@ -297,7 +514,6 @@ "https-browserify": "0.0.1", "inherits": "2.0.3", "insert-module-globals": "7.0.1", - "JSONStream": "1.3.1", "labeled-stream-splicer": "2.0.0", "module-deps": "4.1.1", "os-browserify": "0.1.2", @@ -474,6 +690,12 @@ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -486,11 +708,27 @@ "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc=", "dev": true }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, "caseless": { "version": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", "dev": true }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, "chai": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", @@ -545,16 +783,43 @@ "supports-color": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" } }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "1.3.2", + "async-each": "1.0.1", + "glob-parent": "2.0.0", + "inherits": "2.0.3", + "is-binary-path": "1.0.1", + "is-glob": "2.0.1", + "path-is-absolute": "1.0.1", + "readdirp": "2.1.0" + } + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", "dev": true, "requires": { "inherits": "2.0.3", "safe-buffer": "5.1.1" } }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, "clone": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", @@ -584,6 +849,33 @@ "through2": "2.0.3" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "combine-source-map": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", @@ -607,7 +899,13 @@ "commander": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "concat-stream": { @@ -740,41 +1038,15 @@ "sha.js": "2.4.8" } }, - "cross-spawn-async": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz", - "integrity": "sha1-hF/wwINKPe2dFg2sptOQkGuyiMw=", + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { "lru-cache": "4.1.1", - "which": "1.2.14" - }, - "dependencies": { - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true, - "requires": { - "isexe": "2.0.0" - } - } + "shebang-command": "1.2.0", + "which": "1.3.0" } }, "cryptiles": { @@ -803,12 +1075,6 @@ "randombytes": "2.0.5" } }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, "cssom": { "version": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", @@ -822,6 +1088,15 @@ "cssom": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz" } }, + "d": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "0.10.38" + } + }, "dashdash": { "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", @@ -849,6 +1124,12 @@ "integrity": "sha1-J0Pjq7XD/CRi5SfcpEXgTp9N7hc=", "dev": true }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, "deep-is": { "version": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", @@ -897,6 +1178,12 @@ "defined": "1.0.0" } }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + }, "diffie-hellman": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", @@ -929,12 +1216,6 @@ "readable-stream": "1.1.14" } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, "each-props": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.0.tgz", @@ -977,6 +1258,120 @@ "minimalistic-crypto-utils": "1.0.1" } }, + "emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true + }, + "enhanced-resolve": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", + "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "memory-fs": "0.4.1", + "object-assign": "4.1.1", + "tapable": "0.2.8" + }, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + } + } + }, + "errno": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz", + "integrity": "sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==", + "dev": true, + "requires": { + "prr": "1.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "es5-ext": { + "version": "0.10.38", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.38.tgz", + "integrity": "sha512-jCMyePo7AXbUESwbl8Qi01VSH2piY9s/a3rSU/5w/MlTIx8HPL1xn2InGN8ejt/xulcJgnTO7vqNtOAxzYd2Kg==", + "dev": true, + "requires": { + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.38", + "es6-symbol": "3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.38", + "es6-iterator": "2.0.3", + "es6-set": "0.1.5", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.38", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1", + "event-emitter": "0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.38" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.38", + "es6-iterator": "2.0.3", + "es6-symbol": "3.1.1" + } + }, "escape-string-regexp": { "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", @@ -1005,11 +1400,55 @@ } } }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "0.1.5", + "es6-weak-map": "2.0.2", + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, "esprima": { "version": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", "dev": true }, + "esrecurse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", + "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", + "dev": true, + "requires": { + "estraverse": "4.2.0", + "object-assign": "4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + } + } + }, "estraverse": { "version": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", @@ -1020,6 +1459,16 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1.0.0", + "es5-ext": "0.10.38" + } + }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -1036,39 +1485,44 @@ } }, "execa": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.4.0.tgz", - "integrity": "sha1-TrZGejaglfq7KXD/nV4/t7zm68M=", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "cross-spawn-async": "2.2.5", + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", "is-stream": "1.1.0", - "npm-run-path": "1.0.0", - "object-assign": "4.1.1", - "path-key": "1.0.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", "strip-eof": "1.0.0" - }, - "dependencies": { - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } } }, "exit-on-epipe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", - "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "integrity": "sha1-C92S6H1ShdJn2qgXHQ6wYVlolpI=", "dev": true }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, "express": { "version": "4.13.4", "resolved": "https://registry.npmjs.org/express/-/express-4.13.4.tgz", @@ -1448,6 +1902,15 @@ "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=", "dev": true }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, "extsprintf": { "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", @@ -1520,11 +1983,66 @@ } } }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true + }, "fast-levenshtein": { "version": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, "forever-agent": { "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", @@ -1596,6 +2114,12 @@ "is-property": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" } }, + "get-caller-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", + "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -1620,7 +2144,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "dev": true, "requires": { "fs.realpath": "1.0.0", @@ -1707,6 +2231,25 @@ } } }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, "glogg": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", @@ -1716,6 +2259,12 @@ "sparkles": "1.0.0" } }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, "graceful-readlink": { "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", @@ -5228,7 +5777,7 @@ "gulp-istanbul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/gulp-istanbul/-/gulp-istanbul-1.1.2.tgz", - "integrity": "sha512-53+BDhGlGNHYfeFh/mSXWhNu9wSFmE8qAEFj6ViMiWzTwI9pYxedUxMmGfigwaddsHHQxBl9TgnzUydrX84Kog==", + "integrity": "sha1-r2X6KL/bNXbaq5Xc+qcypqJ8Wgc=", "dev": true, "requires": { "gulp-util": "3.0.8", @@ -8461,6 +9010,12 @@ "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" } }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, "has-gulplog": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", @@ -8490,7 +9045,7 @@ "hash.js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "integrity": "sha1-NA3tvmKQGHFRweodd3o0SJNd+EY=", "dev": true, "requires": { "inherits": "2.0.3", @@ -8548,6 +9103,12 @@ "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", "dev": true }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", + "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", + "dev": true + }, "html-encoding-sniffer": { "version": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.1.tgz", "integrity": "sha1-eb96eF6klf5mFl5zQVPzY/9UN9o=", @@ -8589,12 +9150,6 @@ "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", "dev": true }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -8622,10 +9177,10 @@ "integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=", "dev": true, "requires": { + "JSONStream": "1.3.1", "combine-source-map": "0.7.2", "concat-stream": "1.5.2", "is-buffer": "1.1.5", - "JSONStream": "1.3.1", "lexical-scope": "1.2.0", "process": "0.11.10", "through2": "2.0.3", @@ -8640,12 +9195,93 @@ } } }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "1.11.0" + } + }, "is-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", "dev": true }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, "is-my-json-valid": { "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz", "integrity": "sha1-k27do8o8IR/ZjzstPgjaQ/eykVs=", @@ -8657,6 +9293,15 @@ "xtend": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" } }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, "is-plain-object": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.3.tgz", @@ -8674,11 +9319,29 @@ } } }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, "is-property": { "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", "dev": true }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, "is-typedarray": { "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", @@ -8690,6 +9353,29 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + } + } + }, "isstream": { "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", @@ -8704,6 +9390,30 @@ "jsbn": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" } }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, "jsbn": { "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", @@ -8936,11 +9646,23 @@ } } }, + "json-loader": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", + "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", + "dev": true + }, "json-schema": { "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "dev": true }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "json-stable-stringify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", @@ -8955,6 +9677,12 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -8972,16 +9700,6 @@ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", "dev": true }, - "JSONStream": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz", - "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=", - "dev": true, - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, "jsprim": { "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz", "integrity": "sha1-KnJW9wQSop7jZwqspiWZTE3P8lI=", @@ -8992,6 +9710,15 @@ "verror": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" } }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + }, "labeled-stream-splicer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz", @@ -9003,6 +9730,21 @@ "stream-splicer": "2.0.0" } }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, "levn": { "version": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", @@ -9021,6 +9763,51 @@ "astw": "2.2.0" } }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "strip-bom": "3.0.0" + } + }, + "loader-runner": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", + "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "3.2.0", + "emojis-list": "2.1.0", + "json5": "0.5.1" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, "lodash._basecopy": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", @@ -9075,6 +9862,12 @@ "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", "dev": true }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", + "dev": true + }, "lodash.escape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", @@ -9119,6 +9912,12 @@ "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", "dev": true }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", + "dev": true + }, "lodash.sortby": { "version": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", @@ -9151,13 +9950,71 @@ "lodash.escape": "3.2.0" } }, - "make-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", - "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", "dev": true, "requires": { - "pify": "2.3.0" + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, + "memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "requires": { + "errno": "0.1.6", + "readable-stream": "2.3.3" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } } }, "merge-stream": { @@ -9225,6 +10082,27 @@ } } }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, "miller-rabin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", @@ -9248,6 +10126,12 @@ "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.26.0.tgz" } }, + "mimic-fn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", + "dev": true + }, "minimalistic-assert": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", @@ -9260,12 +10144,38 @@ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", "dev": true }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, "mocha": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.2.tgz", @@ -9539,6 +10449,7 @@ "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", "dev": true, "requires": { + "JSONStream": "1.3.1", "browser-resolve": "1.11.2", "cached-path-relative": "1.0.1", "concat-stream": "1.5.2", @@ -9546,7 +10457,6 @@ "detective": "4.5.0", "duplexer2": "0.1.4", "inherits": "2.0.3", - "JSONStream": "1.3.1", "parents": "1.0.1", "readable-stream": "2.3.3", "resolve": "1.4.0", @@ -9618,6 +10528,111 @@ "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", "dev": true }, + "node-libs-browser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", + "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "dev": true, + "requires": { + "assert": "1.4.1", + "browserify-zlib": "0.2.0", + "buffer": "4.9.1", + "console-browserify": "1.1.0", + "constants-browserify": "1.0.0", + "crypto-browserify": "3.11.1", + "domain-browser": "1.1.7", + "events": "1.1.1", + "https-browserify": "1.0.0", + "os-browserify": "0.3.0", + "path-browserify": "0.0.0", + "process": "0.11.10", + "punycode": "1.4.1", + "querystring-es3": "0.2.1", + "readable-stream": "2.3.3", + "stream-browserify": "2.0.1", + "stream-http": "2.7.2", + "string_decoder": "1.0.3", + "timers-browserify": "2.0.6", + "tty-browserify": "0.0.0", + "url": "0.11.0", + "util": "0.10.3", + "vm-browserify": "0.0.4" + }, + "dependencies": { + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "1.0.6" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "timers-browserify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.6.tgz", + "integrity": "sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==", + "dev": true, + "requires": { + "setimmediate": "1.0.5" + } + } + } + }, "node-pty": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.7.3.tgz", @@ -10660,15 +11675,6 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", @@ -10678,6 +11684,15 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -10781,15 +11796,42 @@ } } }, - "npm-run-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-1.0.0.tgz", - "integrity": "sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8=", + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", "dev": true, "requires": { - "path-key": "1.0.0" + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.1" } }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "1.0.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, "oauth-sign": { "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", @@ -10836,6 +11878,16 @@ } } }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, "object.pick": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.2.0.tgz", @@ -10888,6 +11940,47 @@ "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ=", "dev": true }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, "pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", @@ -10916,16 +12009,49 @@ "pbkdf2": "3.0.12" } }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, "path-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", "dev": true }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, "path-key": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-1.0.0.tgz", - "integrity": "sha1-XVPVeAGWRsDWiADbThRua9wqx68=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, "path-parse": { @@ -10940,6 +12066,15 @@ "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", "dev": true }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "2.3.0" + } + }, "pbkdf2": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", @@ -10983,10 +12118,16 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, "printj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.0.tgz", - "integrity": "sha512-NbiNBOQ0GioHyeD3ni8wZB7ZmfU7mxIrqhWR5XSreX3rUVvk5UOwpzxOnWqrLdCtoBbdQ40sEwC+nXxxjlUo0A==", + "integrity": "sha1-hUh7Xo+WdjsLSiU2E7753Zs4fjw=", "dev": true }, "process": { @@ -11001,6 +12142,12 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", "dev": true }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -11037,10 +12184,51 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "1.1.5" + } + } + } + }, "randombytes": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "integrity": "sha1-3ACaJGuNCaF3tLegrne8Vw9LG3k=", "dev": true, "requires": { "safe-buffer": "5.1.1" @@ -11087,6 +12275,27 @@ } } }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "2.0.0", + "normalize-package-data": "2.4.0", + "path-type": "2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "2.0.0" + } + }, "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", @@ -11099,12 +12308,77 @@ "string_decoder": "0.10.31" } }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "minimatch": "3.0.4", + "readable-stream": "2.3.3", + "set-immediate-shim": "1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "5.1.1" + } + } + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha1-db3FiioUls7EihKDW8VMjVYjNt0=", + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, "remove-trailing-separator": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", "dev": true }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, "replace-ext": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", @@ -11178,6 +12452,18 @@ "tough-cookie": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, "resolve": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", @@ -11187,6 +12473,15 @@ "path-parse": "1.0.5" } }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, "ripemd160": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", @@ -11200,7 +12495,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", "dev": true }, "sax": { @@ -11208,6 +12503,30 @@ "integrity": "sha1-/YYxojvHgmvvXYcb24c3jJVkeCg=", "dev": true }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, "sha.js": { "version": "2.4.8", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", @@ -11227,6 +12546,21 @@ "sha.js": "2.4.8" } }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, "shell-quote": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", @@ -11239,6 +12573,12 @@ "jsonify": "0.0.0" } }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, "sntp": { "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", @@ -11338,6 +12678,12 @@ } } }, + "source-list-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", + "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "dev": true + }, "source-map": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", @@ -11350,6 +12696,33 @@ "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=", "dev": true }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sshpk": { "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.2.tgz", "integrity": "sha1-1agEziJpVRVjjnmNviMnPeBwpfo=", @@ -11475,7 +12848,7 @@ "stream-http": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", + "integrity": "sha1-QKBQ7I3DtTsz2ZCUFcAsC/Gr+60=", "dev": true, "requires": { "builtin-status-codes": "3.0.0", @@ -11565,6 +12938,39 @@ } } }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -11584,6 +12990,12 @@ "ansi-regex": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -11618,14 +13030,11 @@ "acorn": "4.0.13" } }, - "term-size": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-0.1.1.tgz", - "integrity": "sha1-hzYLljlsq1dgljcUzaDQy+7K2co=", - "dev": true, - "requires": { - "execa": "0.4.0" - } + "tapable": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", + "integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=", + "dev": true }, "through": { "version": "2.3.8", @@ -11715,619 +13124,83 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, + "tslib": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz", + "integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=", + "dev": true + }, "tslint": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-4.5.1.tgz", - "integrity": "sha1-BTVocb7yOkNJBnNABvwYgza6gks=", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", "dev": true, "requires": { - "babel-code-frame": "6.22.0", - "colors": "1.1.2", - "diff": "3.3.0", - "findup-sync": "0.3.0", + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.0", + "commander": "2.13.0", + "diff": "3.4.0", "glob": "7.1.2", - "optimist": "0.6.1", - "resolve": "1.3.3", - "tsutils": "1.9.1", - "update-notifier": "2.2.0" + "js-yaml": "3.10.0", + "minimatch": "3.0.4", + "resolve": "1.4.0", + "semver": "5.5.0", + "tslib": "1.8.1", + "tsutils": "2.19.0" }, "dependencies": { - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "2.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "babel-code-frame": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", - "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "color-convert": "1.9.1" } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "boxen": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.1.0.tgz", - "integrity": "sha1-sbad1SIwXoB6md7ud329blFnsQI=", - "dev": true, - "requires": { - "ansi-align": "2.0.0", - "camelcase": "4.1.0", - "chalk": "1.1.3", - "cli-boxes": "1.0.0", - "string-width": "2.1.0", - "term-size": "0.1.1", - "widest-line": "1.0.0" - } - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", - "dev": true - }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", "dev": true, "requires": { - "ansi-styles": "2.2.1", + "ansi-styles": "3.2.0", "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "supports-color": "4.5.0" } }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "commander": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", + "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", "dev": true }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "configstore": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.0.tgz", - "integrity": "sha1-Rd+QcHPibfoc9LLVL1tgVF6qEdE=", - "dev": true, - "requires": { - "dot-prop": "4.1.1", - "graceful-fs": "4.1.11", - "make-dir": "1.0.0", - "unique-string": "1.0.0", - "write-file-atomic": "2.1.0", - "xdg-basedir": "3.0.0" - } - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "1.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", - "dev": true - }, - "diff": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", - "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", - "dev": true - }, - "dot-prop": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.1.1.tgz", - "integrity": "sha1-qEk/C3te7sglJbXHWH+n3nyoWcE=", - "dev": true, - "requires": { - "is-obj": "1.0.1" - } - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "dev": true, - "requires": { - "glob": "5.0.15" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "3.0.2", - "duplexer3": "0.1.4", - "get-stream": "3.0.0", - "is-redirect": "1.0.0", - "is-retry-allowed": "1.1.0", - "is-stream": "1.1.0", - "lowercase-keys": "1.0.0", - "safe-buffer": "5.1.1", - "timed-out": "4.0.1", - "unzip-response": "2.0.1", - "url-parse-lax": "1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "4.0.1" - } - }, - "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" - } - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "6.7.1", - "registry-auth-token": "3.3.1", - "registry-url": "3.1.0", - "semver": "5.3.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "rc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", - "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", - "dev": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "registry-auth-token": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.1.tgz", - "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", - "dev": true, - "requires": { - "rc": "1.2.1", - "safe-buffer": "5.1.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "1.2.1" - } - }, - "resolve": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", - "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", - "dev": true, - "requires": { - "path-parse": "1.0.5" - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "5.3.0" - } - }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "dev": true - }, - "string-width": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.0.tgz", - "integrity": "sha1-AwZkVh/BRslCPsfZeP4kV0N/5tA=", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "update-notifier": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.2.0.tgz", - "integrity": "sha1-G1g3z5DAc22IYncytmHBOPht5y8=", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", "dev": true, "requires": { - "boxen": "1.1.0", - "chalk": "1.1.3", - "configstore": "3.1.0", - "import-lazy": "2.1.0", - "is-npm": "1.0.0", - "latest-version": "3.1.0", - "semver-diff": "2.1.0", - "xdg-basedir": "3.0.0" + "has-flag": "2.0.0" } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "1.0.4" - } - }, - "widest-line": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-1.0.0.tgz", - "integrity": "sha1-DAnIXCqUaD0Nfq+O4JfVZL8OEFw=", - "dev": true, - "requires": { - "string-width": "1.0.2" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "write-file-atomic": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.1.0.tgz", - "integrity": "sha512-0TZ20a+xcIl4u0+Mj5xDH2yOWdmQiXlKf9Hm+TgDXjTMsEYb+gDrmb8e8UNAzMCitX8NBqG4Z/FUQIyzv/R1JQ==", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "slide": "1.1.6" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true } } }, "tsutils": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-1.9.1.tgz", - "integrity": "sha1-ufmrROVa+WgYMdXyjQrur1x1DLA=", - "dev": true + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.19.0.tgz", + "integrity": "sha512-qjgvrvtzRUtOmqlZTfBFtep6s+ymuBLsHOC4ee9JNA+AZIbnR/x4C2Y7QFhOGpD2R3lDp0BBcnxb0vnMVrU7Aw==", + "dev": true, + "requires": { + "tslib": "1.8.1" + } }, "tty-browserify": { "version": "0.0.0", @@ -12361,26 +13234,60 @@ "dev": true }, "typescript": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", - "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", + "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", "dev": true }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "0.5.6", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uglifyjs-webpack-plugin": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz", + "integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=", + "dev": true, + "requires": { + "source-map": "0.5.6", + "uglify-js": "2.8.29", + "webpack-sources": "1.1.0" + } + }, "umd": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.1.tgz", "integrity": "sha1-iuVW4RAR9jwllnCKiDclnwGz1g4=", "dev": true }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "1.0.0" - } - }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -12422,6 +13329,16 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, "verror": { "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", @@ -12609,11 +13526,136 @@ "indexof": "0.0.1" } }, + "watchpack": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", + "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", + "dev": true, + "requires": { + "async": "2.6.0", + "chokidar": "1.7.0", + "graceful-fs": "4.1.11" + } + }, "webidl-conversions": { "version": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.1.tgz", "integrity": "sha1-gBWherg+fhsxFjhIas6B2mziBqA=", "dev": true }, + "webpack": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz", + "integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==", + "dev": true, + "requires": { + "acorn": "5.3.0", + "acorn-dynamic-import": "2.0.2", + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "async": "2.6.0", + "enhanced-resolve": "3.4.1", + "escope": "3.6.0", + "interpret": "1.1.0", + "json-loader": "0.5.7", + "json5": "0.5.1", + "loader-runner": "2.3.0", + "loader-utils": "1.1.0", + "memory-fs": "0.4.1", + "mkdirp": "0.5.1", + "node-libs-browser": "2.1.0", + "source-map": "0.5.6", + "supports-color": "4.5.0", + "tapable": "0.2.8", + "uglifyjs-webpack-plugin": "0.4.6", + "watchpack": "1.4.0", + "webpack-sources": "1.1.0", + "yargs": "8.0.2" + }, + "dependencies": { + "acorn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz", + "integrity": "sha512-Yej+zOJ1Dm/IMZzzj78OntP/r3zHEaKcyNoU2lAaxPtrseM6rF0xwqoz5Q5ysAiED9hTjI2hgtvLXitlCN1/Ug==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "webpack-sources": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", + "integrity": "sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==", + "dev": true, + "requires": { + "source-list-map": "2.0.0", + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "webpack-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-4.0.0.tgz", + "integrity": "sha1-82c92QfW2bHqe/UfzR24W1/Z4PI=", + "dev": true, + "requires": { + "gulp-util": "3.0.8", + "lodash.clone": "4.5.0", + "lodash.some": "4.6.0", + "memory-fs": "0.4.1", + "through": "2.3.8", + "vinyl": "2.1.0", + "webpack": "3.10.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.0.0", + "remove-trailing-separator": "1.0.2", + "replace-ext": "1.0.0" + } + } + } + }, "whatwg-encoding": { "version": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.1.tgz", "integrity": "sha1-PGxFGhmO567FWx7GHQkgxngBpfQ=", @@ -12622,6 +13664,71 @@ "iconv-lite": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" } }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, "xml-name-validator": { "version": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", @@ -12632,12 +13739,103 @@ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "read-pkg-up": "2.0.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "dev": true, + "requires": { + "camelcase": "4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } + } + }, "zmodem.js": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/zmodem.js/-/zmodem.js-0.1.6.tgz", diff --git a/package.json b/package.json index 1dfadf9f..079333de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.0.0", + "version": "3.1.0", "ignore": [ "demo", "test", @@ -66,8 +66,8 @@ "node-pty": "^0.7.2", "nodemon": "1.10.2", "sorcery": "^0.10.0", - "tslint": "^4.0.2", - "typescript": "~2.4.0", + "tslint": "^5.9.1", + "typescript": "~2.7.1", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^3.10.0", @@ -75,17 +75,18 @@ "zmodem.js": "^0.1.5" }, "scripts": { - "prestart": "gulp webpack", "start": "node demo/app", "prestart-zmodem": "npm run build", "start-zmodem": "node build/addons/zmodem/demo/app", "lint": "tslint src/*.ts src/**/*.ts", "test": "gulp test", "build:docs": "jsdoc -c jsdoc.json", + "tsc": "tsc", "build": "gulp build", "prepublish": "npm run build", "coveralls": "gulp coveralls", - "webpack": "gulp webpack" + "webpack": "gulp webpack", + "watch": "gulp watch" }, "dependencies": {} } diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts new file mode 100644 index 00000000..cb915516 --- /dev/null +++ b/src/AccessibilityManager.ts @@ -0,0 +1,287 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as Strings from './Strings'; +import { ITerminal, IBuffer } from './Types'; +import { isMac } from './shared/utils/Browser'; +import { RenderDebouncer } from './utils/RenderDebouncer'; +import { addDisposableListener } from './utils/Dom'; +import { IDisposable } from 'xterm'; + +const MAX_ROWS_TO_READ = 20; +const ACTIVE_ITEM_ID_PREFIX = 'xterm-active-item-'; + +enum BoundaryPosition { + Top, + Bottom +} + +export class AccessibilityManager implements IDisposable { + private _accessibilityTreeRoot: HTMLElement; + private _rowContainer: HTMLElement; + private _rowElements: HTMLElement[] = []; + private _liveRegion: HTMLElement; + private _liveRegionLineCount: number = 0; + + private _renderRowsDebouncer: RenderDebouncer; + + private _topBoundaryFocusListener: (e: FocusEvent) => void; + private _bottomBoundaryFocusListener: (e: FocusEvent) => void; + + private _disposables: IDisposable[] = []; + + /** + * This queue has a character pushed to it for keys that are pressed, if the + * next character added to the terminal is equal to the key char then it is + * not announced (added to live region) because it has already been announced + * by the textarea event (which cannot be canceled). There are some race + * condition cases if there is typing while data is streaming, but this covers + * the main case of typing into the prompt and inputting the answer to a + * question (Y/N, etc.). + */ + private _charsToConsume: string[] = []; + + constructor(private _terminal: ITerminal) { + this._accessibilityTreeRoot = document.createElement('div'); + this._accessibilityTreeRoot.classList.add('xterm-accessibility'); + + this._rowContainer = document.createElement('div'); + this._rowContainer.classList.add('xterm-accessibility-tree'); + for (let i = 0; i < this._terminal.rows; i++) { + this._rowElements[i] = this._createAccessibilityTreeNode(); + this._rowContainer.appendChild(this._rowElements[i]); + } + + this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Top); + this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.Bottom); + this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); + this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); + + this._refreshRowsDimensions(); + this._accessibilityTreeRoot.appendChild(this._rowContainer); + + this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._refreshRows(); + + this._liveRegion = document.createElement('div'); + this._liveRegion.classList.add('live-region'); + this._liveRegion.setAttribute('aria-live', 'assertive'); + this._accessibilityTreeRoot.appendChild(this._liveRegion); + + this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); + + this._disposables.push(this._renderRowsDebouncer); + this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows))); + this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end))); + this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); + // Line feed is an issue as the prompt won't be read out after a command is run + this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); + this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n'))); + this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); + this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar))); + this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); + // TODO: Maybe renderer should fire an event on terminal when the characters change and that + // should be listened to instead? That would mean that the order of events are always + // guarenteed + this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions())); + this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions())); + // This shouldn't be needed on modern browsers but is present in case the + // media query that drives the dprchange event isn't supported + this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions())); + } + + public dispose(): void { + this._terminal.element.removeChild(this._accessibilityTreeRoot); + this._disposables.forEach(d => d.dispose()); + this._disposables = null; + this._accessibilityTreeRoot = null; + this._rowContainer = null; + this._liveRegion = null; + this._rowContainer = null; + this._rowElements = null; + } + + private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void { + const boundaryElement = e.target; + const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2]; + + // Don't scroll if the buffer top has reached the end in that direction + const posInSet = boundaryElement.getAttribute('aria-posinset'); + const lastRowPos = position === BoundaryPosition.Top ? '1' : `${this._terminal.buffer.lines.length}`; + if (posInSet === lastRowPos) { + return; + } + + // Don't scroll when the last focused item was not the second row (focus is going the other + // direction) + if (e.relatedTarget !== beforeBoundaryElement) { + return; + } + + // Remove old boundary element from array + let topBoundaryElement: HTMLElement; + let bottomBoundaryElement: HTMLElement; + if (position === BoundaryPosition.Top) { + topBoundaryElement = boundaryElement; + bottomBoundaryElement = this._rowElements.pop(); + this._rowContainer.removeChild(bottomBoundaryElement); + } else { + topBoundaryElement = this._rowElements.shift(); + bottomBoundaryElement = boundaryElement; + this._rowContainer.removeChild(topBoundaryElement); + } + + // Remove listeners from old boundary elements + topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener); + bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener); + + // Add new element to array/DOM + if (position === BoundaryPosition.Top) { + const newElement = this._createAccessibilityTreeNode(); + this._rowElements.unshift(newElement); + this._rowContainer.insertAdjacentElement('afterbegin', newElement); + } else { + const newElement = this._createAccessibilityTreeNode(); + this._rowElements.push(newElement); + this._rowContainer.appendChild(newElement); + } + + // Add listeners to new boundary elements + this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener); + this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); + + // Scroll up + this._terminal.scrollLines(position === BoundaryPosition.Top ? -1 : 1); + + // Focus new boundary before element + this._rowElements[position === BoundaryPosition.Top ? 1 : this._rowElements.length - 2].focus(); + + // Prevent the standard behavior + e.preventDefault(); + e.stopImmediatePropagation(); + } + + private _onResize(cols: number, rows: number): void { + // Remove bottom boundary listener + this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener); + + // Grow rows as required + for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) { + this._rowElements[i] = this._createAccessibilityTreeNode(); + this._rowContainer.appendChild(this._rowElements[i]); + } + // Shrink rows as required + while (this._rowElements.length > rows) { + this._rowContainer.removeChild(this._rowElements.pop()); + } + + // Add bottom boundary listener + this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener); + + this._refreshRowsDimensions(); + } + + public _createAccessibilityTreeNode(): HTMLElement { + const element = document.createElement('div'); + element.setAttribute('role', 'listitem'); + element.tabIndex = -1; + this._refreshRowDimensions(element); + return element; + } + + private _onTab(spaceCount: number): void { + for (let i = 0; i < spaceCount; i++) { + this._onChar(' '); + } + } + + private _onChar(char: string): void { + if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) { + if (this._charsToConsume.length > 0) { + // Have the screen reader ignore the char if it was just input + const shiftedChar = this._charsToConsume.shift(); + if (shiftedChar !== char) { + this._announceCharacter(char); + } + } else { + this._announceCharacter(char); + } + + if (char === '\n') { + this._liveRegionLineCount++; + if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) { + this._liveRegion.textContent += Strings.tooMuchOutput; + } + } + + // Only detach/attach on mac as otherwise messages can go unaccounced + if (isMac) { + if (this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) { + setTimeout(() => { + this._accessibilityTreeRoot.appendChild(this._liveRegion); + }, 0); + } + } + } + } + + private _clearLiveRegion(): void { + this._liveRegion.textContent = ''; + this._liveRegionLineCount = 0; + + // Only detach/attach on mac as otherwise messages can go unaccounced + if (isMac) { + if (this._liveRegion.parentNode) { + this._accessibilityTreeRoot.removeChild(this._liveRegion); + } + } + } + + private _onKey(keyChar: string): void { + this._clearLiveRegion(); + this._charsToConsume.push(keyChar); + } + + private _refreshRows(start?: number, end?: number): void { + this._renderRowsDebouncer.refresh(start, end); + } + + private _renderRows(start: number, end: number): void { + const buffer: IBuffer = this._terminal.buffer; + const setSize = buffer.lines.length.toString(); + for (let i = start; i <= end; i++) { + const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true); + const posInSet = (buffer.ydisp + i + 1).toString(); + const element = this._rowElements[i]; + element.textContent = lineData.length === 0 ? Strings.blankLine : lineData; + element.setAttribute('aria-posinset', posInSet); + element.setAttribute('aria-setsize', setSize); + } + } + + private _refreshRowsDimensions(): void { + if (!this._terminal.renderer.dimensions.actualCellHeight) { + return; + } + const buffer: IBuffer = this._terminal.buffer; + for (let i = 0; i < this._terminal.rows; i++) { + this._refreshRowDimensions(this._rowElements[i]); + } + } + + private _refreshRowDimensions(element: HTMLElement): void { + element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`; + } + + private _announceCharacter(char: string): void { + if (char === ' ') { + // Always use nbsp for spaces in order to preserve the space between characters in + // voiceover's caption window + this._liveRegion.innerHTML += ' '; + } else { + this._liveRegion.textContent += char; + } + } +} diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 210d2971..0607a573 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; import { Buffer } from './Buffer'; import { CircularList } from './utils/CircularList'; import { MockTerminal } from './utils/TestUtils.test'; diff --git a/src/Buffer.ts b/src/Buffer.ts index 623c3854..7e34a23d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; -import { LineData, CharData } from './Types'; +import { LineData, CharData, ITerminal, IBuffer } from './Types'; export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; @@ -121,11 +120,6 @@ export class Buffer implements IBuffer { if (this._terminal.cols < newCols) { const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? for (let i = 0; i < this._lines.length; i++) { - // TODO: This should be removed, with tests setup for the case that was - // causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824 - if (this._lines.get(i) === undefined) { - this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols)); - } while (this._lines.get(i).length < newCols) { this._lines.get(i).push(ch); } @@ -182,16 +176,13 @@ export class Buffer implements IBuffer { } // Make sure that the cursor stays on screen - if (this.y >= newRows) { - this.y = newRows - 1; - } + this.x = Math.min(this.x, newCols - 1); + this.y = Math.min(this.y, newRows - 1); if (addToY) { this.y += addToY; } - - if (this.x >= newCols) { - this.x = newCols - 1; - } + this.savedY = Math.min(this.savedY, newRows - 1); + this.savedX = Math.min(this.savedX, newCols - 1); this.scrollTop = 0; } @@ -222,7 +213,10 @@ export class Buffer implements IBuffer { // needed here because some chars are 0 characters long (eg. after wide // chars) and some chars are longer than 1 characters long (eg. emojis). let startIndex = startCol; - endCol = endCol || line.length; + // Only set endCol to the line length when it is null. 0 is a valid column. + if (endCol === null) { + endCol = line.length; + } let endIndex = endCol; for (let i = 0; i < line.length; i++) { diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index b9c1824d..009ebf2e 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; import { BufferSet } from './BufferSet'; import { Buffer } from './Buffer'; import { MockTerminal } from './utils/TestUtils.test'; diff --git a/src/BufferSet.ts b/src/BufferSet.ts index e31d2278..553b2056 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, IBufferSet } from './Interfaces'; +import { ITerminal, IBufferSet } from './Types'; import { Buffer } from './Buffer'; import { EventEmitter } from './EventEmitter'; @@ -61,24 +61,35 @@ export class BufferSet extends EventEmitter implements IBufferSet { * Sets the normal Buffer of the BufferSet as its currently active Buffer */ public activateNormalBuffer(): void { + if (this._activeBuffer === this._normal) { + return; + } // The alt buffer should always be cleared when we switch to the normal // buffer. This frees up memory since the alt buffer should always be new // when activated. this._alt.clear(); - this._activeBuffer = this._normal; - this.emit('activate', this._normal); + this.emit('activate', { + activeBuffer: this._normal, + inactiveBuffer: this._alt + }); } /** * Sets the alt Buffer of the BufferSet as its currently active Buffer */ public activateAltBuffer(): void { + if (this._activeBuffer === this._alt) { + return; + } // Since the alt buffer is always cleared when the normal buffer is // activated, we want to fill it when switching to it. this._alt.fillViewportRows(); this._activeBuffer = this._alt; - this.emit('activate', this._alt); + this.emit('activate', { + activeBuffer: this._alt, + inactiveBuffer: this._normal + }); } /** diff --git a/src/CharWidth.ts b/src/CharWidth.ts index d296b435..512ed5f0 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -63,28 +63,33 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu let min = 0; let max = data.length - 1; let mid; - if (ucs < data[0][0] || ucs > data[max][1]) + if (ucs < data[0][0] || ucs > data[max][1]) { return false; + } while (max >= min) { mid = (min + max) >> 1; - if (ucs > data[mid][1]) + if (ucs > data[mid][1]) { min = mid + 1; - else if (ucs < data[mid][0]) + } else if (ucs < data[mid][0]) { max = mid - 1; - else + } else { return true; + } } return false; } function wcwidthBMP(ucs: number): number { // test for 8-bit control characters - if (ucs === 0) + if (ucs === 0) { return opts.nul; - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) + } + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { return opts.control; + } // binary search in table of non-spacing characters - if (bisearch(ucs, COMBINING_BMP)) + if (bisearch(ucs, COMBINING_BMP)) { return 0; + } // if we arrive here, ucs is not a combining or C0/C1 control character if (isWideBMP(ucs)) { return 2; @@ -106,8 +111,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu (ucs >= 0xffe0 && ucs <= 0xffe6))); } function wcwidthHigh(ucs: number): 0 | 1 | 2 { - if (bisearch(ucs, COMBINING_HIGH)) + if (bisearch(ucs, COMBINING_HIGH)) { return 0; + } if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { return 2; } @@ -128,8 +134,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu for (let i = 0; i < CONTAINERSIZE; ++i) { let num = 0; let pos = CODEPOINTS_PER_ITEM; - while (pos--) + while (pos--) { num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); + } table[i] = num; } return table; @@ -148,13 +155,16 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu // ==> n = n & 3 e.g. 000000000000000000000000000000XX return function (num: number): number { num = num | 0; // get asm.js like optimization under V8 - if (num < 32) + if (num < 32) { return control | 0; - if (num < 127) + } + if (num < 127) { return 1; + } let t = table || init_table(); - if (num < 65536) + if (num < 65536) { return t[num >> 4] >> ((num & 15) << 1) & 3; + } // do a full search for high codepoints return wcwidthHigh(num); }; diff --git a/src/Charsets.ts b/src/Charsets.ts index 0ee91d32..fe0112ec 100644 --- a/src/Charsets.ts +++ b/src/Charsets.ts @@ -3,19 +3,19 @@ * @license MIT */ -import { Charset } from './Types'; +import { ICharset } from './Types'; /** * The character sets supported by the terminal. These enable several languages * to be represented within the terminal with only 8-bit encoding. See ISO 2022 * for a discussion on character sets. Only VT100 character sets are supported. */ -export const CHARSETS: { [key: string]: Charset } = {}; +export const CHARSETS: { [key: string]: ICharset } = {}; /** * The default character set, US. */ -export const DEFAULT_CHARSET: Charset = CHARSETS['B']; +export const DEFAULT_CHARSET: ICharset = CHARSETS['B']; /** * DEC Special Character and Line Drawing Set. diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index ca5b4f4b..2aa0449f 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; interface IPosition { start: number; @@ -217,7 +217,7 @@ export class CompositionHelper { if (!dontRecurse) { setTimeout(() => this.updateCompositionElements(true), 0); } - }; + } /** * Clears the textarea's position so that the cursor does not blink on IE. @@ -226,5 +226,5 @@ export class CompositionHelper { private clearTextareaPosition(): void { this.textarea.style.left = ''; this.textarea.style.top = ''; - }; + } } diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts index cb97c368..11cbd84e 100644 --- a/src/EscapeSequences.ts +++ b/src/EscapeSequences.ts @@ -76,4 +76,4 @@ export namespace C0 { export const SP = '\x20'; /** Delete (Caret = ^?) */ export const DEL = '\x7f'; -}; +} diff --git a/src/EventEmitter.test.ts b/src/EventEmitter.test.ts index c1f0a0ab..f2d31cf6 100644 --- a/src/EventEmitter.test.ts +++ b/src/EventEmitter.test.ts @@ -13,18 +13,6 @@ describe('EventEmitter', () => { eventEmitter = new EventEmitter(); }); - describe('once', () => { - it('should trigger the listener only once', () => { - let count = 0; - const listener = () => count++; - eventEmitter.once('test', listener); - eventEmitter.emit('test'); - assert.equal(count, 1); - eventEmitter.emit('test'); - assert.equal(count, 1); - }); - }); - describe('emit', () => { it('should emit events to listeners', () => { let count1 = 0; diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 414eac89..cece1b9d 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { IEventEmitter, IListenerType } from './Interfaces'; +import { XtermListener } from './Types'; +import { IEventEmitter, IDisposable } from 'xterm'; export class EventEmitter implements IEventEmitter { - private _events: {[type: string]: IListenerType[]}; + private _events: {[type: string]: XtermListener[]}; constructor() { // Restore the previous events if available, this will happen if the @@ -14,12 +15,31 @@ export class EventEmitter implements IEventEmitter { this._events = this._events || {}; } - public on(type: string, listener: IListenerType): void { + public on(type: string, listener: XtermListener): void { this._events[type] = this._events[type] || []; this._events[type].push(listener); } - public off(type: string, listener: IListenerType): void { + /** + * Adds a disposabe listener to the EventEmitter, returning the disposable. + * @param type The event type. + * @param handler The handler for the listener. + */ + public addDisposableListener(type: string, handler: XtermListener): IDisposable { + this.on(type, handler); + return { + dispose: () => { + if (!handler) { + // Already disposed + return; + } + this.off(type, handler); + handler = null; + } + }; + } + + public off(type: string, listener: XtermListener): void { if (!this._events[type]) { return; } @@ -28,7 +48,7 @@ export class EventEmitter implements IEventEmitter { let i = obj.length; while (i--) { - if (obj[i] === listener || obj[i].listener === listener) { + if (obj[i] === listener) { obj.splice(i, 1); return; } @@ -41,16 +61,6 @@ export class EventEmitter implements IEventEmitter { } } - public once(type: string, listener: IListenerType): void { - function on(): void { - let args = Array.prototype.slice.call(arguments); - this.off(type, on); - listener.apply(this, args); - } - (on).listener = listener; - this.on(type, on); - } - public emit(type: string, ...args: any[]): void { if (!this._events[type]) { return; @@ -61,7 +71,7 @@ export class EventEmitter implements IEventEmitter { } } - public listeners(type: string): IListenerType[] { + public listeners(type: string): XtermListener[] { return this._events[type] || []; } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1ba6dc7a..18338ae7 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,10 +4,9 @@ * @license MIT */ -import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces'; +import { CharData, IInputHandler, IInputHandlingTerminal, ITerminal } from './Types'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; -import { CharData } from './Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; @@ -26,17 +25,21 @@ export class InputHandler implements IInputHandler { if (char >= ' ') { // calculate print space // expensive call, therefore we save width in line buffer - const ch_width = wcwidth(code); + const chWidth = wcwidth(code); if (this._terminal.charset && this._terminal.charset[char]) { char = this._terminal.charset[char]; } + if (this._terminal.options.screenReaderMode) { + this._terminal.emit('a11y.char', char); + } + let row = this._terminal.buffer.y + this._terminal.buffer.ybase; // insert combining char in last cell // FIXME: needs handling after cursor jumps - if (!ch_width && this._terminal.buffer.x) { + if (!chWidth && this._terminal.buffer.x) { // dont overflow left if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { @@ -56,7 +59,7 @@ export class InputHandler implements IInputHandler { // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 - if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) { + if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) { // autowrap - DECAWM if (this._terminal.wraparoundMode) { this._terminal.buffer.x = 0; @@ -70,8 +73,9 @@ export class InputHandler implements IInputHandler { (this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true; } } else { - if (ch_width === 2) // FIXME: check for xterm behavior + if (chWidth === 2) { // FIXME: check for xterm behavior return; + } } } row = this._terminal.buffer.y + this._terminal.buffer.ybase; @@ -79,7 +83,7 @@ export class InputHandler implements IInputHandler { // insert mode: move characters to right if (this._terminal.insertMode) { // do this twice for a fullwidth char - for (let moves = 0; moves < ch_width; ++moves) { + for (let moves = 0; moves < chWidth; ++moves) { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); @@ -94,12 +98,12 @@ export class InputHandler implements IInputHandler { } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)]; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; this._terminal.buffer.x++; this._terminal.updateRange(this._terminal.buffer.y); // fullwidth char - set next cell width to zero and advance cursor - if (ch_width === 2) { + if (chWidth === 2) { this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined]; this._terminal.buffer.x++; } @@ -162,7 +166,11 @@ export class InputHandler implements IInputHandler { * Horizontal Tab (HT) (Ctrl-I). */ public tab(): void { + const originalX = this._terminal.buffer.x; this._terminal.buffer.x = this._terminal.buffer.nextStop(); + if (this._terminal.options.screenReaderMode) { + this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX); + } } /** @@ -928,7 +936,6 @@ export class InputHandler implements IInputHandler { case 47: // alt screen buffer case 1047: // alt screen buffer this._terminal.buffers.activateAltBuffer(); - this._terminal.selectionManager.setBuffer(this._terminal.buffer); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); break; @@ -1100,7 +1107,6 @@ export class InputHandler implements IInputHandler { // if (params[0] === 1049) { // this.restoreCursor(params); // } - this._terminal.selectionManager.setBuffer(this._terminal.buffer); this._terminal.refresh(0, this._terminal.rows - 1); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); diff --git a/src/Interfaces.ts b/src/Interfaces.ts deleted file mode 100644 index e9a347d8..00000000 --- a/src/Interfaces.ts +++ /dev/null @@ -1,357 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ILinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types'; -import { IColorSet, IRenderer } from './renderer/Interfaces'; -import { IMouseZoneManager } from './input/Interfaces'; - -export interface IBrowser { - isNode: boolean; - userAgent: string; - platform: string; - isFirefox: boolean; - isMSIE: boolean; - isMac: boolean; - isIpad: boolean; - isIphone: boolean; - isMSWindows: boolean; -} - -export interface IBufferAccessor { - buffer: IBuffer; -} - -export interface IElementAccessor { - element: HTMLElement; -} - -export interface ILinkifierAccessor { - linkifier: ILinkifier; -} - -export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter { - selectionManager: ISelectionManager; - charMeasure: ICharMeasure; - textarea: HTMLTextAreaElement; - renderer: IRenderer; - rows: number; - cols: number; - browser: IBrowser; - writeBuffer: string[]; - cursorHidden: boolean; - cursorState: number; - defAttr: number; - options: ITerminalOptions; - buffers: IBufferSet; - isFocused: boolean; - mouseHelper: IMouseHelper; - bracketedPasteMode: boolean; - - /** - * Emit the 'data' event and populate the given data. - * @param data The data to populate in the event. - */ - handler(data: string): void; - scrollLines(disp: number, suppressScrollEvent?: boolean): void; - cancel(ev: Event, force?: boolean): boolean | void; - log(text: string): void; - reset(): void; - showCursor(): void; - blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData; - refresh(start: number, end: number): void; -} - -/** - * This interface encapsulates everything needed from the Terminal by the - * InputHandler. This cleanly separates the large amount of methods needed by - * InputHandler cleanly from the ITerminal interface. - */ -export interface IInputHandlingTerminal extends IEventEmitter { - element: HTMLElement; - options: ITerminalOptions; - cols: number; - rows: number; - charset: Charset; - gcharset: number; - glevel: number; - charsets: Charset[]; - applicationKeypad: boolean; - applicationCursor: boolean; - originMode: boolean; - insertMode: boolean; - wraparoundMode: boolean; - bracketedPasteMode: boolean; - defAttr: number; - curAttr: number; - prefix: string; - savedCols: number; - x10Mouse: boolean; - vt200Mouse: boolean; - normalMouse: boolean; - mouseEvents: boolean; - sendFocus: boolean; - utfMouse: boolean; - sgrMouse: boolean; - urxvtMouse: boolean; - cursorHidden: boolean; - - buffers: IBufferSet; - buffer: IBuffer; - viewport: IViewport; - selectionManager: ISelectionManager; - - bell(): void; - focus(): void; - convertEol: boolean; - updateRange(y: number): void; - scroll(isWrapped?: boolean): void; - setgLevel(g: number): void; - eraseAttr(): number; - eraseRight(x: number, y: number): void; - eraseLine(y: number): void; - eraseLeft(x: number, y: number): void; - blankLine(cur?: boolean, isWrapped?: boolean): LineData; - is(term: string): boolean; - send(data: string): void; - setgCharset(g: number, charset: Charset): void; - resize(x: number, y: number): void; - log(text: string, data?: any): void; - reset(): void; - showCursor(): void; - refresh(start: number, end: number): void; - matchColor(r1: number, g1: number, b1: number): number; - error(text: string, data?: any): void; - setOption(key: string, value: any): void; -} - -export interface ITerminalOptions { - bellSound?: string; - bellStyle?: string; - cancelEvents?: boolean; - cols?: number; - convertEol?: boolean; - cursorBlink?: boolean; - cursorStyle?: string; - debug?: boolean; - disableStdin?: boolean; - enableBold?: boolean; - fontSize?: number; - fontFamily?: string; - handler?: (data: string) => void; - letterSpacing?: number; - lineHeight?: number; - rows?: number; - screenKeys?: boolean; - scrollback?: number; - tabStopWidth?: number; - termName?: string; - theme?: ITheme; - useFlowControl?: boolean; -} - -export interface IBuffer { - lines: ICircularList; - ydisp: number; - ybase: number; - y: number; - x: number; - tabs: any; - scrollBottom: number; - scrollTop: number; - savedY: number; - savedX: number; - isCursorInViewport: boolean; - translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; - nextStop(x?: number): number; - prevStop(x?: number): number; -} - -export interface IBufferSet { - alt: IBuffer; - normal: IBuffer; - active: IBuffer; - - activateNormalBuffer(): void; - activateAltBuffer(): void; -} - -export interface IMouseHelper { - getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; - getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; -} - -export interface IViewport { - syncScrollArea(): void; - onWheel(ev: WheelEvent): void; - onTouchStart(ev: TouchEvent): void; - onTouchMove(ev: TouchEvent): void; - onThemeChanged(colors: IColorSet): void; -} - -export interface ISelectionManager { - selectionText: string; - selectionStart: [number, number]; - selectionEnd: [number, number]; - - disable(): void; - enable(): void; - setBuffer(buffer: IBuffer): void; - setSelection(row: number, col: number, length: number): void; -} - -export interface ICompositionHelper { - compositionstart(): void; - compositionupdate(ev: CompositionEvent): void; - compositionend(): void; - updateCompositionElements(dontRecurse?: boolean): void; - keydown(ev: KeyboardEvent): boolean; -} - -export interface ICharMeasure { - width: number; - height: number; - measure(options: ITerminalOptions): void; -} - -export interface ILinkifier extends IEventEmitter { - attachToDom(mouseZoneManager: IMouseZoneManager): void; - linkifyRows(start: number, end: number): void; - setHypertextLinkHandler(handler: LinkMatcherHandler): void; - setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; - registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; - deregisterLinkMatcher(matcherId: number): boolean; -} - -export interface ICircularList extends IEventEmitter { - length: number; - maxLength: number; - forEach: (callbackfn: (value: T, index: number) => void) => void; - - get(index: number): T; - set(index: number, value: T): void; - push(value: T): void; - pop(): T; - splice(start: number, deleteCount: number, ...items: T[]): void; - trimStart(count: number): void; - shiftElements(start: number, count: number, offset: number): void; -} - -export interface IEventEmitter { - on(type: string, listener: IListenerType): void; - off(type: string, listener: IListenerType): void; - emit(type: string, data?: any): void; -} - -export interface IListenerType { - (data?: any): void; - listener?: (data?: any) => void; -}; - -export interface ILinkMatcherOptions { - /** - * The index of the link from the regex.match(text) call. This defaults to 0 - * (for regular expressions without capture groups). - */ - matchIndex?: number; - /** - * A callback that validates an individual link, returning true if valid and - * false if invalid. - */ - validationCallback?: LinkMatcherValidationCallback; - /** - * A callback that fires when the mouse hovers over a link. - */ - tooltipCallback?: LinkMatcherHandler; - /** - * A callback that fires when the mouse leaves a link that was hovered. - */ - leaveCallback?: () => void; - /** - * The priority of the link matcher, this defines the order in which the link - * matcher is evaluated relative to others, from highest to lowest. The - * default value is 0. - */ - priority?: number; -} - -/** - * Handles actions generated by the parser. - */ -export interface IInputHandler { - addChar(char: string, code: number): void; - - /** C0 BEL */ bell(): void; - /** C0 LF */ lineFeed(): void; - /** C0 CR */ carriageReturn(): void; - /** C0 BS */ backspace(): void; - /** C0 HT */ tab(): void; - /** C0 SO */ shiftOut(): void; - /** C0 SI */ shiftIn(): void; - - /** CSI @ */ insertChars(params?: number[]): void; - /** CSI A */ cursorUp(params?: number[]): void; - /** CSI B */ cursorDown(params?: number[]): void; - /** CSI C */ cursorForward(params?: number[]): void; - /** CSI D */ cursorBackward(params?: number[]): void; - /** CSI E */ cursorNextLine(params?: number[]): void; - /** CSI F */ cursorPrecedingLine(params?: number[]): void; - /** CSI G */ cursorCharAbsolute(params?: number[]): void; - /** CSI H */ cursorPosition(params?: number[]): void; - /** CSI I */ cursorForwardTab(params?: number[]): void; - /** CSI J */ eraseInDisplay(params?: number[]): void; - /** CSI K */ eraseInLine(params?: number[]): void; - /** CSI L */ insertLines(params?: number[]): void; - /** CSI M */ deleteLines(params?: number[]): void; - /** CSI P */ deleteChars(params?: number[]): void; - /** CSI S */ scrollUp(params?: number[]): void; - /** CSI T */ scrollDown(params?: number[]): void; - /** CSI X */ eraseChars(params?: number[]): void; - /** CSI Z */ cursorBackwardTab(params?: number[]): void; - /** CSI ` */ charPosAbsolute(params?: number[]): void; - /** CSI a */ HPositionRelative(params?: number[]): void; - /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; - /** CSI c */ sendDeviceAttributes(params?: number[]): void; - /** CSI d */ linePosAbsolute(params?: number[]): void; - /** CSI e */ VPositionRelative(params?: number[]): void; - /** CSI f */ HVPosition(params?: number[]): void; - /** CSI g */ tabClear(params?: number[]): void; - /** CSI h */ setMode(params?: number[]): void; - /** CSI l */ resetMode(params?: number[]): void; - /** CSI m */ charAttributes(params?: number[]): void; - /** CSI n */ deviceStatus(params?: number[]): void; - /** CSI p */ softReset(params?: number[]): void; - /** CSI q */ setCursorStyle(params?: number[]): void; - /** CSI r */ setScrollRegion(params?: number[]): void; - /** CSI s */ saveCursor(params?: number[]): void; - /** CSI u */ restoreCursor(params?: number[]): void; -} - -export interface ITheme { - foreground?: string; - background?: string; - cursor?: string; - cursorAccent?: string; - selection?: string; - black?: string; - red?: string; - green?: string; - yellow?: string; - blue?: string; - magenta?: string; - cyan?: string; - white?: string; - brightBlack?: string; - brightRed?: string; - brightGreen?: string; - brightYellow?: string; - brightBlue?: string; - brightMagenta?: string; - brightCyan?: string; - brightWhite?: string; -} -export interface ISoundManager { - playBellSound(): void; -} diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 0878aafc..14b1ac0f 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -4,10 +4,9 @@ */ import { assert } from 'chai'; -import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces'; +import { IMouseZoneManager, IMouseZone } from './input/Types'; +import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types'; import { Linkifier } from './Linkifier'; -import { LinkMatcher, LineData } from './Types'; -import { IMouseZoneManager, IMouseZone } from './input/Interfaces'; import { MockBuffer } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; @@ -17,7 +16,7 @@ class TestLinkifier extends Linkifier { Linkifier.TIME_BEFORE_LINKIFY = 0; } - public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; } + public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; } public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); } } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index e6a63b5c..a9617952 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces'; -import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types'; -import { IMouseZoneManager } from './input/Interfaces'; +import { IMouseZoneManager } from './input/Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types'; import { MouseZone } from './input/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; @@ -44,7 +43,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { */ protected static TIME_BEFORE_LINKIFY = 200; - protected _linkMatchers: LinkMatcher[] = []; + protected _linkMatchers: ILinkMatcher[] = []; private _mouseZoneManager: IMouseZoneManager; private _rowsTimeoutId: number; @@ -82,12 +81,12 @@ export class Linkifier extends EventEmitter implements ILinkifier { } // Increase range to linkify - if (!this._rowsToLinkify.start) { + if (this._rowsToLinkify.start === null) { this._rowsToLinkify.start = start; this._rowsToLinkify.end = end; } else { - this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start; - this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end; + this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start); + this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end); } // Clear out any existing links on this row range @@ -143,7 +142,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { throw new Error('handler must be defined'); } - const matcher: LinkMatcher = { + const matcher: ILinkMatcher = { id: this._nextLinkMatcherId++, regex, handler, @@ -151,6 +150,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { validationCallback: options.validationCallback, hoverTooltipCallback: options.tooltipCallback, hoverLeaveCallback: options.leaveCallback, + willLinkActivate: options.willLinkActivate, priority: options.priority || 0 }; this._addLinkMatcherToList(matcher); @@ -163,7 +163,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * considered after older link matchers. * @param matcher The link matcher to be added. */ - private _addLinkMatcherToList(matcher: LinkMatcher): void { + private _addLinkMatcherToList(matcher: ILinkMatcher): void { if (this._linkMatchers.length === 0) { this._linkMatchers.push(matcher); return; @@ -219,7 +219,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param offset The how much of the row has already been linkified. * @return The link element(s) that were added. */ - private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void { + private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void { // Iterate over nodes as we want to consider text nodes let result = []; const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; @@ -264,7 +264,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param uri The URI of the link. * @param matcher The link matcher for the link. */ - private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void { + private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void { this._mouseZoneManager.add(new MouseZone( x + 1, x + 1 + uri.length, @@ -276,21 +276,27 @@ export class Linkifier extends EventEmitter implements ILinkifier { window.open(uri, '_blank'); }, e => { - this.emit(LinkHoverEventTypes.HOVER, { x, y, length: uri.length}); + this.emit(LinkHoverEventTypes.HOVER, { x, y, length: uri.length}); this._terminal.element.style.cursor = 'pointer'; }, e => { - this.emit(LinkHoverEventTypes.TOOLTIP, { x, y, length: uri.length}); + this.emit(LinkHoverEventTypes.TOOLTIP, { x, y, length: uri.length}); if (matcher.hoverTooltipCallback) { matcher.hoverTooltipCallback(e, uri); } }, () => { - this.emit(LinkHoverEventTypes.LEAVE, { x, y, length: uri.length}); + this.emit(LinkHoverEventTypes.LEAVE, { x, y, length: uri.length}); this._terminal.element.style.cursor = ''; if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); } + }, + e => { + if (matcher.willLinkActivate) { + return matcher.willLinkActivate(e, uri); + } + return true; } )); } diff --git a/src/Parser.ts b/src/Parser.ts index 6f4b0932..03b4a39e 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,7 +5,7 @@ */ import { C0 } from './EscapeSequences'; -import { IInputHandler } from './Interfaces'; +import { IInputHandler } from './Types'; import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {}; @@ -224,8 +224,9 @@ export class Parser { ch += data.charAt(this._position + 1); } // surrogate low - already handled above - if (0xDC00 <= code && code <= 0xDFFF) + if (0xDC00 <= code && code <= 0xDFFF) { continue; + } switch (this._state) { case ParserState.NORMAL: diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index a10dc669..a76947e5 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -5,14 +5,13 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; -import { ITerminal, ICircularList, IBuffer } from './Interfaces'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; +import { LineData, CharData, ITerminal, ICircularList, IBuffer } from './Types'; import { MockTerminal } from './utils/TestUtils.test'; -import { LineData, CharData } from './Types'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -21,16 +20,15 @@ class TestMockTerminal extends MockTerminal { class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, - buffer: IBuffer, charMeasure: CharMeasure ) { - super(terminal, buffer, charMeasure); + super(terminal, charMeasure); } public get model(): SelectionModel { return this._model; } public selectLineAt(line: number): void { this._selectLineAt(line); } - public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); } + public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } // Disable DOM interaction public enable(): void {} @@ -59,7 +57,7 @@ describe('SelectionManager', () => { terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; buffer = terminal.buffer; - selectionManager = new TestSelectionManager(terminal, buffer, null); + selectionManager = new TestSelectionManager(terminal, null); }); function stringToRow(text: string): LineData { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index cd6fc996..eaf813c9 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,15 +3,15 @@ * @license MIT */ +import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData, XtermListener } from './Types'; import { MouseHelper } from './utils/MouseHelper'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; -import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces'; import { SelectionModel } from './SelectionModel'; -import { LineData, CharData } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { AltClickHandler } from './handlers/AltClickHandler'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -29,6 +29,12 @@ const DRAG_SCROLL_MAX_SPEED = 15; */ const DRAG_SCROLL_INTERVAL = 50; +/** + * The maximum amount of time that can have elapsed for an alt click to move the + * cursor. + */ +const ALT_CLICK_MOVE_CURSOR_TIME = 500; + /** * A string containing all characters that are considered word separated by the * double click to select work logic. @@ -95,10 +101,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; + private _trimListener: XtermListener; + + private _mouseDownTimeStamp: number; constructor( private _terminal: ITerminal, - private _buffer: IBuffer, private _charMeasure: CharMeasure ) { super(); @@ -109,18 +117,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._activeSelectionMode = SelectionMode.NORMAL; } + private get _buffer(): IBuffer { + return this._terminal.buffers.active; + } + /** * Initializes listener variables. */ private _initListeners(): void { this._mouseMoveListener = event => this._onMouseMove(event); this._mouseUpListener = event => this._onMouseUp(event); + this._trimListener = (amount: number) => this._onTrim(amount); - // Only adjust the selection on trim, shiftElements is rarely used (only in - // reverseIndex) and delete in a splice is only ever used when the same - // number of elements was just added. Given this is could actually be - // beneficial to leave the selection as is for these cases. - this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount)); + this.initBuffersListeners(); + } + + public initBuffersListeners(): void { + this._terminal.buffer.lines.on('trim', this._trimListener); + this._terminal.buffers.on('activate', e => this._onBufferActivate(e)); } /** @@ -139,16 +153,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._enabled = true; } - /** - * Sets the active buffer, this should be called when the alt buffer is - * switched in or out. - * @param buffer The active buffer. - */ - public setBuffer(buffer: IBuffer): void { - this._buffer = buffer; - this.clearSelection(); - } - public get selectionStart(): [number, number] { return this._model.finalSelectionStart; } public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; } @@ -249,6 +253,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd }); } + /** + * Checks if the current click was inside the current selection + * @param event The mouse event + */ + public isClickInSelection(event: MouseEvent): boolean { + const coords = this._getMouseBufferCoords(event); + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + + if (!start || !end) { + return false; + } + + return (coords[1] > start[1] && coords[1] < end[1]) || + (start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]); + } + + /** + * Selects word at the current mouse event coordinates. + * @param event The mouse event. + */ + public selectWordAtCursor(event: MouseEvent): void { + const coords = this._getMouseBufferCoords(event); + if (coords) { + this._selectWordAt(coords, false); + this._model.selectionEnd = null; + this.refresh(true); + } + } + /** * Selects all text within the terminal. */ @@ -274,7 +309,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); + const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -294,7 +329,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1]; + let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; @@ -322,6 +357,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mousedown event. */ public onMouseDown(event: MouseEvent): void { + this._mouseDownTimeStamp = event.timeStamp; // If we have selection, we want the context menu on right click even if the // terminal is in mouse mode. if (event.button === 2 && this.hasSelection) { @@ -440,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager const coords = this._getMouseBufferCoords(event); if (coords) { this._activeSelectionMode = SelectionMode.WORD; - this._selectWordAt(coords); + this._selectWordAt(coords, true); } } @@ -541,10 +577,25 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouseup event. */ private _onMouseUp(event: MouseEvent): void { + let timeElapsed = event.timeStamp - this._mouseDownTimeStamp; + this._removeMouseDownListeners(); - if (this.hasSelection) + if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { + (new AltClickHandler(event, this._terminal)).move(); + } else if (this.hasSelection) { this._terminal.emit('selection'); + } + } + + private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void { + this.clearSelection(); + // Only adjust the selection on trim, shiftElements is rarely used (only in + // reverseIndex) and delete in a splice is only ever used when the same + // number of elements was just added. Given this is could actually be + // beneficial to leave the selection as is for these cases. + e.inactiveBuffer.lines.off('trim', this._trimListener); + e.activeBuffer.lines.on('trim', this._trimListener); } /** @@ -582,7 +633,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * Gets positional information for the word at the coordinated specified. * @param coords The coordinates to get the word at. */ - private _getWordAt(coords: [number, number]): IWordPosition { + private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition { const bufferLine = this._buffer.lines.get(coords[1]); if (!bufferLine) { return null; @@ -688,15 +739,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager - leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) - rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis) + if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') { + return null; + } + return { start, length }; } /** * Selects the word at the coordinates specified. * @param coords The coordinates to get the word at. + * @param allowWhitespaceOnlySelection If whitespace should be selected */ - protected _selectWordAt(coords: [number, number]): void { - const wordPosition = this._getWordAt(coords); + protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { + const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); if (wordPosition) { this._model.selectionStart = [wordPosition.start, coords[1]]; this._model.selectionStartLength = wordPosition.length; @@ -708,7 +764,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param coords The coordinates to get the word at. */ private _selectToWordAt(coords: [number, number]): void { - const wordPosition = this._getWordAt(coords); + const wordPosition = this._getWordAt(coords, true); if (wordPosition) { this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]]; } diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index e195d6bd..ed483dbe 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -4,9 +4,9 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; import { SelectionModel } from './SelectionModel'; -import {BufferSet} from './BufferSet'; +import { BufferSet } from './BufferSet'; import { MockTerminal } from './utils/TestUtils.test'; class TestSelectionModel extends SelectionModel { diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index 5982b1e9..a9a3c89e 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal } from './Types'; /** * Represents a selection within the buffer. This model only cares about column diff --git a/src/SoundManager.ts b/src/SoundManager.ts index b95a253e..dc61acbd 100644 --- a/src/SoundManager.ts +++ b/src/SoundManager.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISoundManager } from './Interfaces'; +import { ITerminal, ISoundManager } from './Types'; // Source: https://freesound.org/people/altemark/sounds/45759/ // This sound is released under the Creative Commons Attribution 3.0 Unported diff --git a/src/Strings.ts b/src/Strings.ts new file mode 100644 index 00000000..86f0ebe1 --- /dev/null +++ b/src/Strings.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export let blankLine = 'Blank line'; +export let promptLabel = 'Terminal input'; +export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read'; diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index 00db4dd6..283c5bc0 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -15,17 +15,17 @@ import { assert } from 'chai'; import { Terminal } from './Terminal'; import { CHAR_DATA_CHAR_INDEX } from './Buffer'; -let primitive_pty: any; +let primitivePty: any; // fake sychronous pty write - read // we just pipe the data from slave to master as a child program would do // pty.js opens pipe fds with O_NONBLOCK // just wait 10ms instead of setting fds to blocking mode function ptyWriteRead(data: string, cb: (result: string) => void): void { - fs.writeSync(primitive_pty.slave, data); + fs.writeSync(primitivePty.slave, data); setTimeout(() => { let b = new Buffer(64000); - let bytes = fs.readSync(primitive_pty.master, b, 0, 64000, null); + let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null); cb(b.toString('utf8', 0, bytes)); }); } @@ -37,7 +37,7 @@ function ptyReset(cb: (result: string) => void): void { /* debug helpers */ // generate colorful noisy output to compare xterm and emulator cell states -function formatError(in_: string, out_: string, expected: string): string { +function formatError(input: string, output: string, expected: string): string { function addLineNumber(start: number, color: string): (s: string) => string { let counter = start || 0; return function(s: string): string { @@ -47,9 +47,9 @@ function formatError(in_: string, out_: string, expected: string): string { } let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890'; let s = ''; - s += '\n\x1b[34m' + JSON.stringify(in_); + s += '\n\x1b[34m' + JSON.stringify(input); s += '\n\x1b[33m ' + line80 + '\n'; - s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n'); + s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n'); s += '\n\x1b[33m ' + line80 + '\n'; s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n'); return s; @@ -58,15 +58,15 @@ function formatError(in_: string, out_: string, expected: string): string { // simple debug output of terminal cells function terminalToString(term: Terminal): string { let result = ''; - let line_s = ''; + let lineText = ''; for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { - line_s = ''; + lineText = ''; for (let cell = 0; cell < term.cols; ++cell) { - line_s += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX]; + lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX]; } // rtrim empty cells as xterm does - line_s = line_s.replace(/\s+$/, ''); - result += line_s; + lineText = lineText.replace(/\s+$/, ''); + result += lineText; result += '\n'; } return result; @@ -83,7 +83,7 @@ if (os.platform() !== 'win32') { /** some helpers for pty interaction */ // we need a pty in between to get the termios decorations // for the basic test cases a raw pty device is enough - primitive_pty = pty.native.open(COLS, ROWS); + primitivePty = pty.native.open(COLS, ROWS); /** tests */ describe('xterm output comparison', () => { @@ -118,24 +118,24 @@ if (os.platform() !== 'win32') { ((filename: string) => { it(filename.split('/').slice(-1)[0], done => { ptyReset(() => { - let in_file = fs.readFileSync(filename, 'utf8'); - ptyWriteRead(in_file, from_pty => { + let inFile = fs.readFileSync(filename, 'utf8'); + ptyWriteRead(inFile, fromPty => { // uncomment this to get log from terminal // console.log = function(){}; // Perform a synchronous .write(data) - xterm.writeBuffer.push(from_pty); + xterm.writeBuffer.push(fromPty); xterm.innerWrite(); - let from_emulator = terminalToString(xterm); + let fromEmulator = terminalToString(xterm); console.log = CONSOLE_LOG; let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); // Some of the tests have whitespace on the right of lines, we trim all the linex // from xterm.js so ignore this for now at least. let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n'); - if (from_emulator !== expectedRightTrimmed) { + if (fromEmulator !== expectedRightTrimmed) { // uncomment to get noisy output - throw new Error(formatError(in_file, from_emulator, expected)); + throw new Error(formatError(inFile, fromEmulator, expected)); // throw new Error('mismatch'); } done(); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 9cb0b75c..12467d84 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -503,6 +503,9 @@ describe('term.js addons', () => { it('should return \\x1b[5C for alt+right', () => { assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C }); + it('should return \\x1ba for alt+a', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba'); + }); }); describe('On macOS platforms', () => { @@ -515,6 +518,19 @@ describe('term.js addons', () => { it('should return \\x1bf for alt+right', () => { assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C }); + it('should return undefined for alt+a', () => { + assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined); + }); + }); + + describe('with macOptionIsMeta', () => { + beforeEach(() => { + term.browser.isMac = true; + term.setOption('macOptionIsMeta', true); + }); + it('should return \\x1ba for alt+a', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba'); + }); }); it('should return \\x1b[5A for alt+up', () => { @@ -562,6 +578,99 @@ describe('term.js addons', () => { assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~'); assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~'); }); + + // Characters using ctrl+alt sequences + it('should return proper sequence for ctrl+alt+a', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01'); + }); + + // Characters using alt sequences (numbers) + it('should return proper sequences for alt+0', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)'); + }); + it('should return proper sequences for alt+1', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!'); + }); + it('should return proper sequences for alt+2', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@'); + }); + it('should return proper sequences for alt+3', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#'); + }); + it('should return proper sequences for alt+4', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$'); + }); + it('should return proper sequences for alt+5', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%'); + }); + it('should return proper sequences for alt+6', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^'); + }); + it('should return proper sequences for alt+7', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&'); + }); + it('should return proper sequences for alt+8', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*'); + }); + it('should return proper sequences for alt+9', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b('); + }); + + // Characters using alt sequences (special chars) + it('should return proper sequences for alt+;', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:'); + }); + it('should return proper sequences for alt+=', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b='); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+'); + }); + it('should return proper sequences for alt+,', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<'); + }); + it('should return proper sequences for alt+-', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_'); + }); + it('should return proper sequences for alt+.', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>'); + }); + it('should return proper sequences for alt+/', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?'); + }); + it('should return proper sequences for alt+~', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~'); + }); + it('should return proper sequences for alt+[', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b['); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{'); + }); + it('should return proper sequences for alt+\\', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|'); + }); + it('should return proper sequences for alt+]', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}'); + }); + it('should return proper sequences for alt+\'', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\''); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"'); + }); }); describe('Third level shift', () => { @@ -597,6 +706,22 @@ describe('term.js addons', () => { }; }); + describe('with macOptionIsMeta', () => { + beforeEach(() => { + term.browser.isMac = true; + term.setOption('macOptionIsMeta', true); + }); + + it('should interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + assert.equal(term.keyDown(evKeyDown), false); + }); + }); + describe('On Mac OS', () => { beforeEach(() => { term.browser.isMac = true; diff --git a/src/Terminal.ts b/src/Terminal.ts index f12f42b7..9041fb8d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,6 +21,9 @@ * http://linux.die.net/man/7/urxvt */ +import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; +import { IMouseZoneManager } from './input/Types'; +import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; @@ -35,17 +38,44 @@ import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; import { CharMeasure } from './utils/CharMeasure'; -import * as Browser from './utils/Browser'; +import * as Browser from './shared/utils/Browser'; +import * as Strings from './Strings'; import { MouseHelper } from './utils/MouseHelper'; import { CHARSETS } from './Charsets'; -import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces'; import { DefaultBellSound, SoundManager } from './SoundManager'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; -import { IMouseZoneManager } from './input/Interfaces'; import { MouseZoneManager } from './input/MouseZoneManager'; -import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; -import { IRenderer } from './renderer/Interfaces'; +import { AccessibilityManager } from './AccessibilityManager'; +import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; +import { ITheme, ILocalizableStrings } from 'xterm'; + +// reg + shift key mappings for digits and special chars +const KEYCODE_KEY_MAPPINGS = { + // digits 0-9 + 48: ['0', ')'], + 49: ['1', '!'], + 50: ['2', '@'], + 51: ['3', '#'], + 52: ['4', '$'], + 53: ['5', '%'], + 54: ['6', '^'], + 55: ['7', '&'], + 56: ['8', '*'], + 57: ['9', '('], + + // special chars + 186: [';', ':'], + 187: ['=', '+'], + 188: [',', '<'], + 189: ['-', '_'], + 190: ['.', '>'], + 191: ['/', '?'], + 192: ['`', '~'], + 219: ['[', '{'], + 220: ['\\', '|'], + 221: [']', '}'], + 222: ['\'', '"'] +}; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -75,16 +105,22 @@ const DEFAULT_OPTIONS: ITerminalOptions = { enableBold: true, fontFamily: 'courier-new, courier, monospace', fontSize: 15, + fontWeight: 'normal', + fontWeightBold: 'bold', lineHeight: 1.0, letterSpacing: 0, scrollback: 1000, screenKeys: false, + screenReaderMode: false, debug: false, + macOptionIsMeta: false, cancelEvents: false, disableStdin: false, useFlowControl: false, + allowTransparency: false, tabStopWidth: 8, - theme: null + theme: null, + rightClickSelectsWord: Browser.isMac // programFeatures: false, // focusKeys: false, }; @@ -92,6 +128,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; + public screenElement: HTMLElement; /** * The HTMLElement that the terminal is created in, set by Terminal.open. @@ -131,10 +168,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // charset // The current charset - public charset: Charset; + public charset: ICharset; public gcharset: number; public glevel: number; - public charsets: Charset[]; + public charsets: ICharset[]; // mouse properties private decLocator: boolean; // This is unstable and never set @@ -181,7 +218,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private writeStopped: boolean; // leftover surrogate high from previous write invocation - private surrogate_high: string; + private surrogateHigh: string; // Store if user went browsing history in scrollback private userScrolling: boolean; @@ -190,15 +227,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public soundManager: SoundManager; private parser: Parser; public renderer: IRenderer; + public selectionManager: SelectionManager; public linkifier: ILinkifier; public buffers: BufferSet; - public buffer: Buffer; public viewport: IViewport; private compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; private _mouseZoneManager: IMouseZoneManager; public mouseHelper: MouseHelper; - public selectionManager: SelectionManager; + private _accessibilityManager: AccessibilityManager; + private _screenDprMonitor: ScreenDprMonitor; public cols: number; public rows: number; @@ -281,7 +319,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.xoffSentToCatchUp = false; this.writeStopped = false; - this.surrogate_high = ''; + this.surrogateHigh = ''; this.userScrolling = false; this.inputHandler = new InputHandler(this); @@ -295,17 +333,23 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); - this.buffer = this.buffers.active; // Convenience shortcut; - this.buffers.on('activate', (buffer: Buffer) => { - this.buffer = buffer; - }); - - // Ensure the selection manager has the correct buffer if (this.selectionManager) { - this.selectionManager.setBuffer(this.buffer); + this.selectionManager.clearSelection(); + this.selectionManager.initBuffersListeners(); } } + /** + * Convenience property to active buffer. + */ + public get buffer(): Buffer { + return this.buffers.active; + } + + public static get strings(): ILocalizableStrings { + return Strings; + } + /** * back_color_erase feature for xterm. */ @@ -363,6 +407,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT value = 'block'; } break; + case 'fontWeight': + if (!value) { + value = 'normal'; + } + break; + case 'fontWeightBold': + if (!value) { + value = 'bold'; + } + break; case 'lineHeight': if (value < 1) { console.warn(`${key} cannot be less than 1, value: ${value}`); @@ -416,15 +470,30 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'enableBold': case 'letterSpacing': case 'lineHeight': + case 'fontWeight': + case 'fontWeightBold': + const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold' || key === 'enableBold'); + // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); - this.renderer.onResize(this.cols, this.rows, false); + this.renderer.onResize(this.cols, this.rows, didCharSizeChange); this.refresh(0, this.rows - 1); - // this.charMeasure.measure(this.options); case 'scrollback': this.buffers.resize(this.cols, this.rows); this.viewport.syncScrollArea(); break; + case 'screenReaderMode': + if (value) { + if (!this._accessibilityManager) { + this._accessibilityManager = new AccessibilityManager(this); + } + } else { + if (this._accessibilityManager) { + this._accessibilityManager.dispose(); + this._accessibilityManager = null; + } + } + break; case 'tabStopWidth': this.buffers.setupTabStops(); break; } // Inform renderer of changes @@ -443,7 +512,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.element.classList.add('focus'); this.showCursor(); this.emit('focus'); - }; + } /** * Blur the terminal, calling the blur function on the terminal's underlying @@ -457,6 +526,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Binds the desired blur behavior on a given terminal object. */ private _onTextAreaBlur(): void { + // Text can safely be removed on blur. Doing it earlier could interfere with + // screen readers reading it out. + this.textarea.value = ''; this.refresh(this.buffer.y, this.buffer.y); if (this.sendFocus) { this.send(C0.ESC + '[O'); @@ -489,12 +561,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Firefox doesn't appear to fire the contextmenu event on right click on(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.selectionManager); + rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); } }); } else { on(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.selectionManager); + rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); }); } @@ -537,16 +609,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } }, true); - on(this.textarea, 'keydown', (ev: KeyboardEvent) => { - this._keyDown(ev); - }, true); - - on(this.textarea, 'keypress', (ev: KeyboardEvent) => { - this._keyPress(ev); - // Truncate the textarea's value, since it is not needed - this.textarea.value = ''; - }, true); - + on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true); + on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true); on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart()); on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e)); on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend()); @@ -574,7 +638,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.document = this.parent.ownerDocument; this.body = this.document.body; - initializeCharAtlas(this.document); + this._screenDprMonitor = new ScreenDprMonitor(); + this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio)); // Create main element container this.element = this.document.createElement('div'); @@ -593,18 +658,24 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewportScrollArea.classList.add('xterm-scroll-area'); this.viewportElement.appendChild(this.viewportScrollArea); - this._mouseZoneManager = new MouseZoneManager(this); - this.on('scroll', () => this._mouseZoneManager.clearAll()); - this.linkifier.attachToDom(this._mouseZoneManager); - + this.screenElement = document.createElement('div'); + this.screenElement.classList.add('xterm-screen'); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. this.helperContainer = document.createElement('div'); this.helperContainer.classList.add('xterm-helpers'); - fragment.appendChild(this.helperContainer); + this.screenElement.appendChild(this.helperContainer); + fragment.appendChild(this.screenElement); + + this._mouseZoneManager = new MouseZoneManager(this); + this.on('scroll', () => this._mouseZoneManager.clearAll()); + this.linkifier.attachToDom(this._mouseZoneManager); this.textarea = document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); + // TODO: New API to set title? This could say "Terminal bash input", etc. + this.textarea.setAttribute('aria-label', Strings.promptLabel); + this.textarea.setAttribute('aria-multiline', 'false'); this.textarea.setAttribute('autocorrect', 'off'); this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('spellcheck', 'false'); @@ -634,10 +705,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); this.on('blur', () => this.renderer.onBlur()); this.on('focus', () => this.renderer.onFocus()); + this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)); + // dprchange should handle this case, we need this as well for browsers that don't support the + // matchMedia query. + window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); - this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure); + this.selectionManager = new SelectionManager(this, this.charMeasure); this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)); this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end)); this.selectionManager.on('newselection', text => { @@ -656,6 +731,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.mouseHelper = new MouseHelper(this.renderer); + if (this.options.screenReaderMode) { + // Note that this must be done *after* the renderer is created in order to + // ensure the correct order of the dprchange event + this._accessibilityManager = new AccessibilityManager(this); + } + // Measure the character size this.charMeasure.measure(this.options); @@ -716,7 +797,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT button = getButton(ev); // get mouse coordinates - pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -742,7 +823,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -1023,7 +1104,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT */ public refresh(start: number, end: number): void { if (this.renderer) { - this.renderer.queueRefresh(start, end); + this.renderer.refreshRows(start, end); } } @@ -1370,7 +1451,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return this.cancel(ev, true); } - if (isThirdLevelShift(this.browser, ev)) { + if (this._isThirdLevelShift(this.browser, ev)) { return true; } @@ -1391,6 +1472,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return this.cancel(ev, true); } + private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { + const thirdLevelKey = + (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + + if (ev.type === 'keypress') { + return thirdLevelKey; + } + + // Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events) + return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); + } + /** * Returns an object that determines how a KeyboardEvent should be handled. The key of the * returned value is the new key code to pass to the PTY. @@ -1550,21 +1644,23 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT break; case 36: // home - if (modifiers) + if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; - else if (this.applicationCursor) + } else if (this.applicationCursor) { result.key = C0.ESC + 'OH'; - else + } else { result.key = C0.ESC + '[H'; + } break; case 35: // end - if (modifiers) + if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; - else if (this.applicationCursor) + } else if (this.applicationCursor) { result.key = C0.ESC + 'OF'; - else + } else { result.key = C0.ESC + '[F'; + } break; case 33: // page up @@ -1691,14 +1787,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // ^] - Operating System Command (OSC) result.key = String.fromCharCode(29); } - } else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) { - // On Mac this is a third level shift. Use instead. - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32); - } else if (ev.keyCode === 192) { - result.key = C0.ESC + '`'; - } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - result.key = C0.ESC + (ev.keyCode - 48); + } else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.metaKey) { + // On macOS this is a third level shift when !macOptionIsMeta. Use instead. + const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode]; + const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1]; + if (key) { + result.key = C0.ESC + key; + } else if (ev.keyCode >= 65 && ev.keyCode <= 90) { + const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32; + result.key = C0.ESC + String.fromCharCode(keyCode); } } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) { if (ev.keyCode === 65) { // cmd + a @@ -1725,7 +1822,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param g * @param charset */ - public setgCharset(g: number, charset: Charset): void { + public setgCharset(g: number, charset: ICharset): void { this.charsets[g] = charset; if (this.glevel === g) { this.charset = charset; @@ -1758,7 +1855,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } if (!key || ( - (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this.browser, ev) + (ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev) )) { return false; } @@ -1853,7 +1950,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.rows = y; this.buffers.setupTabStops(this.cols); - this.charMeasure.measure(this.options); + if (this.charMeasure) { + this.charMeasure.measure(this.options); + } this.refresh(0, this.rows - 1); this.emit('resize', {cols: x, rows: y}); @@ -2074,11 +2173,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.options.cols = this.cols; const customKeyEventHandler = this.customKeyEventHandler; const inputHandler = this.inputHandler; - const buffers = this.buffers; this.setup(); this.customKeyEventHandler = customKeyEventHandler; this.inputHandler = inputHandler; - this.buffers = buffers; this.refresh(0, this.rows - 1); this.viewport.syncScrollArea(); } @@ -2107,14 +2204,17 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } private visualBell(): boolean { - return this.options.bellStyle === 'visual' || - this.options.bellStyle === 'both'; + return false; + // return this.options.bellStyle === 'visual' || + // this.options.bellStyle === 'both'; } private soundBell(): boolean { - return this.options.bellStyle === 'sound' || - this.options.bellStyle === 'both'; + return this.options.bellStyle === 'sound'; + // return this.options.bellStyle === 'sound' || + // this.options.bellStyle === 'both'; } +} /** * Helpers @@ -2135,19 +2235,6 @@ function off(el: any, type: string, handler: (event: Event) => any, capture: boo el.removeEventListener(type, handler, capture); } -function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { - const thirdLevelKey = - (browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); - - if (ev.type === 'keypress') { - return thirdLevelKey; - } - - // Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events) - return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); -} - function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean { return ev.keyCode === 16 || // Shift ev.keyCode === 17 || // Ctrl @@ -2200,7 +2287,7 @@ function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: return Math.pow(30 * (r1 - r2), 2) + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); -}; +} function matchColor_(r1: number, g1: number, b1: number): number { diff --git a/src/Types.ts b/src/Types.ts index a0c03fdd..be292dbf 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,33 +3,353 @@ * @license MIT */ -export type LinkMatcher = { - id: number, - regex: RegExp, - handler: LinkMatcherHandler, - hoverTooltipCallback?: LinkMatcherHandler, - hoverLeaveCallback?: () => void, - matchIndex?: number, - validationCallback?: LinkMatcherValidationCallback, - priority?: number -}; -export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; -export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; +import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter as IPublicEventEmitter, IEventEmitter } from 'xterm'; +import { IColorSet, IRenderer } from './renderer/Types'; +import { IMouseZoneManager } from './input/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; -export type Charset = {[key: string]: string}; + +export type XtermListener = (...args: any[]) => void; export type CharData = [number, string, number, number]; export type LineData = CharData[]; -export type LinkHoverEvent = { - x: number, - y: number, - length: number -}; +export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; +export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; export enum LinkHoverEventTypes { HOVER = 'linkhover', TOOLTIP = 'linktooltip', LEAVE = 'linkleave' -}; +} + +/** + * This interface encapsulates everything needed from the Terminal by the + * InputHandler. This cleanly separates the large amount of methods needed by + * InputHandler cleanly from the ITerminal interface. + */ +export interface IInputHandlingTerminal extends IEventEmitter { + element: HTMLElement; + options: ITerminalOptions; + cols: number; + rows: number; + charset: ICharset; + gcharset: number; + glevel: number; + charsets: ICharset[]; + applicationKeypad: boolean; + applicationCursor: boolean; + originMode: boolean; + insertMode: boolean; + wraparoundMode: boolean; + bracketedPasteMode: boolean; + defAttr: number; + curAttr: number; + prefix: string; + savedCols: number; + x10Mouse: boolean; + vt200Mouse: boolean; + normalMouse: boolean; + mouseEvents: boolean; + sendFocus: boolean; + utfMouse: boolean; + sgrMouse: boolean; + urxvtMouse: boolean; + cursorHidden: boolean; + + buffers: IBufferSet; + buffer: IBuffer; + viewport: IViewport; + selectionManager: ISelectionManager; + + bell(): void; + focus(): void; + convertEol: boolean; + updateRange(y: number): void; + scroll(isWrapped?: boolean): void; + setgLevel(g: number): void; + eraseAttr(): number; + eraseRight(x: number, y: number): void; + eraseLine(y: number): void; + eraseLeft(x: number, y: number): void; + blankLine(cur?: boolean, isWrapped?: boolean): LineData; + is(term: string): boolean; + send(data: string): void; + setgCharset(g: number, charset: ICharset): void; + resize(x: number, y: number): void; + log(text: string, data?: any): void; + reset(): void; + showCursor(): void; + refresh(start: number, end: number): void; + matchColor(r1: number, g1: number, b1: number): number; + error(text: string, data?: any): void; + setOption(key: string, value: any): void; +} + +export interface IViewport { + scrollBarWidth: number; + syncScrollArea(): void; + onWheel(ev: WheelEvent): void; + onTouchStart(ev: TouchEvent): void; + onTouchMove(ev: TouchEvent): void; + onThemeChanged(colors: IColorSet): void; +} + +export interface ICompositionHelper { + compositionstart(): void; + compositionupdate(ev: CompositionEvent): void; + compositionend(): void; + updateCompositionElements(dontRecurse?: boolean): void; + keydown(ev: KeyboardEvent): boolean; +} + +/** + * Handles actions generated by the parser. + */ +export interface IInputHandler { + addChar(char: string, code: number): void; + + /** C0 BEL */ bell(): void; + /** C0 LF */ lineFeed(): void; + /** C0 CR */ carriageReturn(): void; + /** C0 BS */ backspace(): void; + /** C0 HT */ tab(): void; + /** C0 SO */ shiftOut(): void; + /** C0 SI */ shiftIn(): void; + + /** CSI @ */ insertChars(params?: number[]): void; + /** CSI A */ cursorUp(params?: number[]): void; + /** CSI B */ cursorDown(params?: number[]): void; + /** CSI C */ cursorForward(params?: number[]): void; + /** CSI D */ cursorBackward(params?: number[]): void; + /** CSI E */ cursorNextLine(params?: number[]): void; + /** CSI F */ cursorPrecedingLine(params?: number[]): void; + /** CSI G */ cursorCharAbsolute(params?: number[]): void; + /** CSI H */ cursorPosition(params?: number[]): void; + /** CSI I */ cursorForwardTab(params?: number[]): void; + /** CSI J */ eraseInDisplay(params?: number[]): void; + /** CSI K */ eraseInLine(params?: number[]): void; + /** CSI L */ insertLines(params?: number[]): void; + /** CSI M */ deleteLines(params?: number[]): void; + /** CSI P */ deleteChars(params?: number[]): void; + /** CSI S */ scrollUp(params?: number[]): void; + /** CSI T */ scrollDown(params?: number[]): void; + /** CSI X */ eraseChars(params?: number[]): void; + /** CSI Z */ cursorBackwardTab(params?: number[]): void; + /** CSI ` */ charPosAbsolute(params?: number[]): void; + /** CSI a */ HPositionRelative(params?: number[]): void; + /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; + /** CSI c */ sendDeviceAttributes(params?: number[]): void; + /** CSI d */ linePosAbsolute(params?: number[]): void; + /** CSI e */ VPositionRelative(params?: number[]): void; + /** CSI f */ HVPosition(params?: number[]): void; + /** CSI g */ tabClear(params?: number[]): void; + /** CSI h */ setMode(params?: number[]): void; + /** CSI l */ resetMode(params?: number[]): void; + /** CSI m */ charAttributes(params?: number[]): void; + /** CSI n */ deviceStatus(params?: number[]): void; + /** CSI p */ softReset(params?: number[]): void; + /** CSI q */ setCursorStyle(params?: number[]): void; + /** CSI r */ setScrollRegion(params?: number[]): void; + /** CSI s */ saveCursor(params?: number[]): void; + /** CSI u */ restoreCursor(params?: number[]): void; +} + +export interface ILinkMatcher { + id: number; + regex: RegExp; + handler: LinkMatcherHandler; + hoverTooltipCallback?: LinkMatcherHandler; + hoverLeaveCallback?: () => void; + matchIndex?: number; + validationCallback?: LinkMatcherValidationCallback; + priority?: number; + willLinkActivate?: (event: MouseEvent, uri: string) => boolean; +} + +export interface ICharset { + [key: string]: string; +} + +export interface ILinkHoverEvent { + x: number; + y: number; + length: number; +} + +export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { + screenElement: HTMLElement; + selectionManager: ISelectionManager; + charMeasure: ICharMeasure; + renderer: IRenderer; + browser: IBrowser; + writeBuffer: string[]; + cursorHidden: boolean; + cursorState: number; + defAttr: number; + options: ITerminalOptions; + buffer: IBuffer; + buffers: IBufferSet; + isFocused: boolean; + mouseHelper: IMouseHelper; + viewport: IViewport; + bracketedPasteMode: boolean; + applicationCursor: boolean; + + /** + * Emit the 'data' event and populate the given data. + * @param data The data to populate in the event. + */ + handler(data: string): void; + send(data: string): void; + scrollLines(disp: number, suppressScrollEvent?: boolean): void; + cancel(ev: Event, force?: boolean): boolean | void; + log(text: string): void; + showCursor(): void; + blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData; +} + +export interface IBufferAccessor { + buffer: IBuffer; +} + +export interface IElementAccessor { + element: HTMLElement; +} + +export interface ILinkifierAccessor { + linkifier: ILinkifier; +} + +export interface IMouseHelper { + getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; +} + +export interface ICharMeasure { + width: number; + height: number; + measure(options: ITerminalOptions): void; +} + +// TODO: The options that are not in the public API should be reviewed +export interface ITerminalOptions extends IPublicTerminalOptions { + cancelEvents?: boolean; + convertEol?: boolean; + debug?: boolean; + handler?: (data: string) => void; + screenKeys?: boolean; + termName?: string; + useFlowControl?: boolean; +} + +export interface IBuffer { + lines: ICircularList; + ydisp: number; + ybase: number; + y: number; + x: number; + tabs: any; + scrollBottom: number; + scrollTop: number; + savedY: number; + savedX: number; + isCursorInViewport: boolean; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + nextStop(x?: number): number; + prevStop(x?: number): number; +} + +export interface IBufferSet extends IEventEmitter { + alt: IBuffer; + normal: IBuffer; + active: IBuffer; + + activateNormalBuffer(): void; + activateAltBuffer(): void; +} + +export interface ICircularList extends IEventEmitter { + length: number; + maxLength: number; + forEach: (callbackfn: (value: T, index: number) => void) => void; + + get(index: number): T; + set(index: number, value: T): void; + push(value: T): void; + pop(): T; + splice(start: number, deleteCount: number, ...items: T[]): void; + trimStart(count: number): void; + shiftElements(start: number, count: number, offset: number): void; +} + +export interface ISelectionManager { + selectionText: string; + selectionStart: [number, number]; + selectionEnd: [number, number]; + + disable(): void; + enable(): void; + setSelection(row: number, col: number, length: number): void; + isClickInSelection(event: MouseEvent): boolean; + selectWordAtCursor(event: MouseEvent): void; +} + +export interface ILinkifier extends IEventEmitter { + attachToDom(mouseZoneManager: IMouseZoneManager): void; + linkifyRows(start: number, end: number): void; + setHypertextLinkHandler(handler: LinkMatcherHandler): void; + setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; + registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; + deregisterLinkMatcher(matcherId: number): boolean; +} + +export interface ILinkMatcherOptions { + /** + * The index of the link from the regex.match(text) call. This defaults to 0 + * (for regular expressions without capture groups). + */ + matchIndex?: number; + /** + * A callback that validates an individual link, returning true if valid and + * false if invalid. + */ + validationCallback?: LinkMatcherValidationCallback; + /** + * A callback that fires when the mouse hovers over a link. + */ + tooltipCallback?: LinkMatcherHandler; + /** + * A callback that fires when the mouse leaves a link that was hovered. + */ + leaveCallback?: () => void; + /** + * The priority of the link matcher, this defines the order in which the link + * matcher is evaluated relative to others, from highest to lowest. The + * default value is 0. + */ + priority?: number; + /** + * A callback that fires when the mousedown and click events occur that + * determines whether a link will be activated upon click. This enables + * only activating a link when a certain modifier is held down, if not the + * mouse event will continue propagation (eg. double click to select word). + */ + willLinkActivate?: (event: MouseEvent, uri: string) => boolean; +} + +export interface IBrowser { + isNode: boolean; + userAgent: string; + platform: string; + isFirefox: boolean; + isMSIE: boolean; + isMac: boolean; + isIpad: boolean; + isIphone: boolean; + isMSWindows: boolean; +} + +export interface ISoundManager { + playBellSound(): void; +} diff --git a/src/Viewport.ts b/src/Viewport.ts index 1ea7fa0b..35a2aa68 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -3,15 +3,18 @@ * @license MIT */ -import { ITerminal, IViewport } from './Interfaces'; +import { IColorSet } from './renderer/Types'; +import { ITerminal, IViewport } from './Types'; import { CharMeasure } from './utils/CharMeasure'; -import { IColorSet } from './renderer/Interfaces'; + +const FALLBACK_SCROLL_BAR_WIDTH = 15; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. * Logic for the virtual scroll bar is included in this object. */ export class Viewport implements IViewport { + public scrollBarWidth: number = 0; private currentRowHeight: number = 0; private lastRecordedBufferLength: number = 0; private lastRecordedViewportHeight: number = 0; @@ -31,6 +34,10 @@ export class Viewport implements IViewport { private scrollArea: HTMLElement, private charMeasure: CharMeasure ) { + // Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar. + // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, + // therefore we account for a standard amount to make it visible + this.scrollBarWidth = (this.viewportElement.offsetWidth - this.scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); // Perform this async to ensure the CharMeasure is ready. @@ -48,13 +55,8 @@ export class Viewport implements IViewport { private refresh(): void { if (this.charMeasure.height > 0) { this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; - - if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) { - this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight; - this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px'; - } - - const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength); + this.lastRecordedViewportHeight = this.viewportElement.offsetHeight; + const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight); if (this.lastRecordedBufferHeight !== newBufferHeight) { this.lastRecordedBufferHeight = newBufferHeight; this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px'; @@ -93,6 +95,12 @@ export class Viewport implements IViewport { * @param ev The scroll event. */ private onScroll(ev: Event): void { + // Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt + // which causes the terminal to scroll the buffer to the top + if (!this.viewportElement.offsetParent) { + return; + } + const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); const diff = newRow - this.terminal.buffer.ydisp; this.terminal.scrollLines(diff, true); @@ -119,7 +127,7 @@ export class Viewport implements IViewport { this.viewportElement.scrollTop += ev.deltaY * multiplier; // Prevent the page from scrolling when the terminal scrolls ev.preventDefault(); - }; + } /** * Handles the touchstart event, recording the touch occurred. @@ -127,7 +135,7 @@ export class Viewport implements IViewport { */ public onTouchStart(ev: TouchEvent): void { this.lastTouchY = ev.touches[0].pageY; - }; + } /** * Handles the touchmove event, scrolling the viewport if the position shifted. @@ -141,5 +149,5 @@ export class Viewport implements IViewport { } this.viewportElement.scrollTop += deltaY; ev.preventDefault(); - }; + } } diff --git a/src/addons/attach/Interfaces.ts b/src/addons/attach/Interfaces.ts new file mode 100644 index 00000000..ab809af9 --- /dev/null +++ b/src/addons/attach/Interfaces.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. + */ + +import { Terminal } from 'xterm'; + +export interface IAttachAddonTerminal extends Terminal { + __socket?: WebSocket; + __attachSocketBuffer?: string; + + __getMessage?(ev: MessageEvent): void; + __flushBuffer?(): void; + __pushToBuffer?(data: string): void; + __sendData?(data: string): void; +} diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts index 9bf0e070..018cfb31 100644 --- a/src/addons/attach/attach.test.ts +++ b/src/addons/attach/attach.test.ts @@ -5,14 +5,14 @@ import { assert, expect } from 'chai'; -import * as attach from './attach' +import * as attach from './attach'; class MockTerminal {} describe('attach addon', () => { describe('apply', () => { it('should do register the `attach` and `detach` methods', () => { - attach.apply(MockTerminal); + attach.apply(MockTerminal); assert.equal(typeof (MockTerminal).prototype.attach, 'function'); assert.equal(typeof (MockTerminal).prototype.detach, 'function'); }); diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index 4e0d5941..cbfd4a24 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -5,118 +5,118 @@ * Implements the attach method, that attaches the terminal to a WebSocket stream. */ +/// + +import { Terminal } from 'xterm'; +import { IAttachAddonTerminal } from './Interfaces'; + /** * Attaches the given terminal to the given socket. * - * @param {Terminal} term - The terminal to be attached to the given socket. - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. + * @param term The terminal to be attached to the given socket. + * @param socket The socket to attach the current terminal. + * @param bidirectional Whether the terminal should send data to the socket as well. + * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. */ -export function attach(term, socket, bidirectional, buffered) { - bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional; - term.socket = socket; +export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { + const addonTerminal = term; + bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; + addonTerminal.__socket = socket; - term._flushBuffer = function() { - term.write(term._attachSocketBuffer); - term._attachSocketBuffer = null; + addonTerminal.__flushBuffer = () => { + addonTerminal.write(addonTerminal.__attachSocketBuffer); + addonTerminal.__attachSocketBuffer = null; }; - term._pushToBuffer = function(data) { - if (term._attachSocketBuffer) { - term._attachSocketBuffer += data; + addonTerminal.__pushToBuffer = (data: string) => { + if (addonTerminal.__attachSocketBuffer) { + addonTerminal.__attachSocketBuffer += data; } else { - term._attachSocketBuffer = data; - setTimeout(term._flushBuffer, 10); + addonTerminal.__attachSocketBuffer = data; + setTimeout(addonTerminal.__flushBuffer, 10); } }; - var myTextDecoder; + let myTextDecoder; - term._getMessage = function(ev) { - var str; - if (typeof ev.data === "object") { + addonTerminal.__getMessage = function(ev: MessageEvent): void { + let str; + if (typeof ev.data === 'object') { if (ev.data instanceof ArrayBuffer) { if (!myTextDecoder) { myTextDecoder = new TextDecoder(); } str = myTextDecoder.decode( ev.data ); - } - else { - throw "TODO: handle Blob?"; + } else { + throw 'TODO: handle Blob?'; } } if (buffered) { - term._pushToBuffer(str || ev.data); + addonTerminal.__pushToBuffer(str || ev.data); } else { - term.write(str || ev.data); + addonTerminal.write(str || ev.data); } }; - term._sendData = function(data) { + addonTerminal.__sendData = (data: string) => { if (socket.readyState !== 1) { return; } socket.send(data); }; - socket.addEventListener('message', term._getMessage); + socket.addEventListener('message', addonTerminal.__getMessage); if (bidirectional) { - term.on('data', term._sendData); + addonTerminal.on('data', addonTerminal.__sendData); } - socket.addEventListener('close', term.detach.bind(term, socket)); - socket.addEventListener('error', term.detach.bind(term, socket)); -}; + socket.addEventListener('close', () => detach(addonTerminal, socket)); + socket.addEventListener('error', () => detach(addonTerminal, socket)); +} /** * Detaches the given terminal from the given socket * - * @param {Terminal} term - The terminal to be detached from the given socket. - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. + * @param term The terminal to be detached from the given socket. + * @param socket The socket from which to detach the current terminal. */ -export function detach(term, socket) { - term.off('data', term._sendData); +export function detach(term: Terminal, socket: WebSocket): void { + const addonTerminal = term; + addonTerminal.off('data', addonTerminal.__sendData); - socket = (typeof socket == 'undefined') ? term.socket : socket; + socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; if (socket) { - socket.removeEventListener('message', term._getMessage); + socket.removeEventListener('message', addonTerminal.__getMessage); } - delete term.socket; -}; + delete addonTerminal.__socket; +} -export function apply(terminalConstructor) { +export function apply(terminalConstructor: typeof Terminal): void { /** * Attaches the current terminal to the given socket * - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. + * @param socket The socket to attach the current terminal. + * @param bidirectional Whether the terminal should send data to the socket as well. + * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. */ - terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) { - return attach(this, socket, bidirectional, buffered); + (terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { + attach(this, socket, bidirectional, buffered); }; /** * Detaches the current terminal from the given socket. * - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. + * @param socket The socket from which to detach the current terminal. */ - terminalConstructor.prototype.detach = function(socket) { - return detach(this, socket); + (terminalConstructor.prototype).detach = function (socket: WebSocket): void { + detach(this, socket); }; } diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json index 97722324..a4e8e78b 100644 --- a/src/addons/attach/tsconfig.json +++ b/src/addons/attach/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/attach/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/fit/fit.test.ts b/src/addons/fit/fit.test.ts index 159cc987..9a6d89fd 100644 --- a/src/addons/fit/fit.test.ts +++ b/src/addons/fit/fit.test.ts @@ -5,14 +5,14 @@ import { assert, expect } from 'chai'; -import * as fit from './fit' +import * as fit from './fit'; class MockTerminal {} describe('fit addon', () => { describe('apply', () => { it('should do register the `proposeGeometry` and `fit` methods', () => { - fit.apply(MockTerminal); + fit.apply(MockTerminal); assert.equal(typeof (MockTerminal).prototype.proposeGeometry, 'function'); assert.equal(typeof (MockTerminal).prototype.fit, 'function'); }); diff --git a/src/addons/fit/fit.ts b/src/addons/fit/fit.ts index b87ccc07..f6f593dc 100644 --- a/src/addons/fit/fit.ts +++ b/src/addons/fit/fit.ts @@ -13,43 +13,57 @@ * row and truncate its width with the current number of columns). */ -export function proposeGeometry(term) { +/// + +import { Terminal } from 'xterm'; + +export interface IGeometry { + rows: number; + cols: number; +} + +export function proposeGeometry(term: Terminal): IGeometry { if (!term.element.parentElement) { return null; } - var parentElementStyle = window.getComputedStyle(term.element.parentElement); - var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); - var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17); - var elementStyle = window.getComputedStyle(term.element); - var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')); - var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')); - var availableHeight = parentElementHeight - elementPaddingVer; - var availableWidth = parentElementWidth - elementPaddingHor; - var geometry = { - cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth), - rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight) + const parentElementStyle = window.getComputedStyle(term.element.parentElement); + const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); + const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); + const elementStyle = window.getComputedStyle(term.element); + const elementPadding = { + top: parseInt(elementStyle.getPropertyValue('padding-top')), + bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), + right: parseInt(elementStyle.getPropertyValue('padding-right')), + left: parseInt(elementStyle.getPropertyValue('padding-left')) + }; + const elementPaddingVer = elementPadding.top + elementPadding.bottom; + const elementPaddingHor = elementPadding.right + elementPadding.left; + const availableHeight = parentElementHeight - elementPaddingVer; + const availableWidth = parentElementWidth - elementPaddingHor - (term).viewport.scrollBarWidth; + const geometry = { + cols: Math.floor(availableWidth / (term).renderer.dimensions.actualCellWidth), + rows: Math.floor(availableHeight / (term).renderer.dimensions.actualCellHeight) }; - return geometry; -}; +} -export function fit(term) { - var geometry = proposeGeometry(term); +export function fit(term: Terminal): void { + const geometry = proposeGeometry(term); if (geometry) { // Force a full render if (term.rows !== geometry.rows || term.cols !== geometry.cols) { - term.renderer.clear(); + (term).renderer.clear(); term.resize(geometry.cols, geometry.rows); } } -}; +} -export function apply(terminalConstructor) { - terminalConstructor.prototype.proposeGeometry = function() { +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).proposeGeometry = function (): IGeometry { return proposeGeometry(this); - } + }; - terminalConstructor.prototype.fit = function() { - return fit(this); - } + (terminalConstructor.prototype).fit = function (): void { + fit(this); + }; } diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index 949f8b60..78b3c410 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/fit/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/fullscreen/fullscreen.test.ts b/src/addons/fullscreen/fullscreen.test.ts index c77ea75c..bb98bd30 100644 --- a/src/addons/fullscreen/fullscreen.test.ts +++ b/src/addons/fullscreen/fullscreen.test.ts @@ -5,14 +5,14 @@ import { assert, expect } from 'chai'; -import * as fullscreen from './fullscreen' +import * as fullscreen from './fullscreen'; class MockTerminal {} describe('fullscreen addon', () => { describe('apply', () => { it('should do register the `toggleFullscreen` method', () => { - fullscreen.apply(MockTerminal); + fullscreen.apply(MockTerminal); assert.equal(typeof (MockTerminal).prototype.toggleFullScreen, 'function'); }); }); diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 31ab5189..47440667 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -3,15 +3,19 @@ * @license MIT */ +/// + +import { Terminal } from 'xterm'; + /** * Toggle the given terminal's fullscreen mode. - * @param {Terminal} term - The terminal to toggle full screen mode - * @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false) + * @param term The terminal to toggle full screen mode + * @param fullscreen Toggle fullscreen on (true) or off (false) */ -export function toggleFullScreen(term, fullscreen) { - var fn; +export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { + let fn: string; - if (typeof fullscreen == 'undefined') { + if (typeof fullscreen === 'undefined') { fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add'; } else if (!fullscreen) { fn = 'remove'; @@ -20,10 +24,10 @@ export function toggleFullScreen(term, fullscreen) { } term.element.classList[fn]('fullscreen'); -}; +} -export function apply(terminalConstructor) { - terminalConstructor.prototype.toggleFullScreen = function (fullscreen) { - return toggleFullScreen(this, fullscreen); +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void { + toggleFullScreen(this, fullscreen); }; } diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index b04144c1..35ecb7ce 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/fullscreen/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts new file mode 100644 index 00000000..6faa03e2 --- /dev/null +++ b/src/addons/search/Interfaces.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal } from 'xterm'; + +export interface ISearchAddonTerminal extends Terminal { + __searchHelper?: ISearchHelper; + + // TODO: Reuse ITerminal from core + buffer: any; + selectionManager: any; +} + +export interface ISearchHelper { + findNext(term: string): boolean; + findPrevious(term: string): boolean; +} diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 5735dd48..2abc3b38 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -3,8 +3,7 @@ * @license MIT */ -// import { ITerminal } from '../../Interfaces'; -// import { translateBufferLineToString } from '../../utils/BufferLine'; +import { ISearchHelper, ISearchAddonTerminal } from './Interfaces'; interface ISearchResult { term: string; @@ -15,8 +14,8 @@ interface ISearchResult { /** * A class that knows how to search the terminal and how to display the results. */ -export class SearchHelper { - constructor(private _terminal: any) { +export class SearchHelper implements ISearchHelper { + constructor(private _terminal: ISearchAddonTerminal) { // TODO: Search for multiple instances on 1 line // TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted // TODO: Highlight other instances in the viewport @@ -114,8 +113,23 @@ export class SearchHelper { private _findInLine(term: string, y: number): ISearchResult { const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase(); const lowerTerm = term.toLowerCase(); - const searchIndex = lowerStringLine.indexOf(lowerTerm); + let searchIndex = lowerStringLine.indexOf(lowerTerm); if (searchIndex >= 0) { + const line = this._terminal.buffer.lines.get(y); + for (let i = 0; i < searchIndex; i++) { + const charData = line[i]; + // Adjust the searchIndex to normalize emoji into single chars + const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; + if (char.length > 1) { + searchIndex -= char.length - 1; + } + // Adjust the searchIndex for empty characters following wide unicode + // chars (eg. CJK) + const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; + if (charWidth === 0) { + searchIndex++; + } + } return { term, col: searchIndex, @@ -134,7 +148,7 @@ export class SearchHelper { return false; } this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length); - this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp, false); + this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp); return true; } } diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index 480fea7f..5c0b2b96 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -3,8 +3,11 @@ * @license MIT */ -import { SearchHelper } from './SearchHelper'; +/// +import { SearchHelper } from './SearchHelper'; +import { Terminal } from 'xterm'; +import { ISearchAddonTerminal } from './Interfaces'; /** * Find the next instance of the term, then scroll to and select it. If it @@ -12,12 +15,13 @@ import { SearchHelper } from './SearchHelper'; * @param term Tne search term. * @return Whether a result was found. */ -export function findNext(terminal: any, term: string): boolean { - if (!terminal._searchHelper) { - terminal.searchHelper = new SearchHelper(terminal); +export function findNext(terminal: Terminal, term: string): boolean { + const addonTerminal = terminal; + if (!addonTerminal.__searchHelper) { + addonTerminal.__searchHelper = new SearchHelper(addonTerminal); } - return (terminal.searchHelper).findNext(term); -}; + return addonTerminal.__searchHelper.findNext(term); +} /** * Find the previous instance of the term, then scroll to and select it. If it @@ -25,19 +29,20 @@ export function findNext(terminal: any, term: string): boolean { * @param term Tne search term. * @return Whether a result was found. */ -export function findPrevious(terminal: any, term: string): boolean { - if (!terminal._searchHelper) { - terminal.searchHelper = new SearchHelper(terminal); - } - return (terminal.searchHelper).findPrevious(term); -}; - -export function apply(terminalConstructor) { - terminalConstructor.prototype.findNext = function(term) { - return findNext(this, term); - } - - terminalConstructor.prototype.findPrevious = function(term) { - return findPrevious(this, term); +export function findPrevious(terminal: Terminal, term: string): boolean { + const addonTerminal = terminal; + if (!addonTerminal.__searchHelper) { + addonTerminal.__searchHelper = new SearchHelper(addonTerminal); } + return addonTerminal.__searchHelper.findPrevious(term); +} + +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).findNext = function(term: string): boolean { + return findNext(this, term); + }; + + (terminalConstructor.prototype).findPrevious = function(term: string): boolean { + return findPrevious(this, term); + }; } diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 7f72e47f..d1c3d7e0 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/search/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/terminado/Interfaces.ts b/src/addons/terminado/Interfaces.ts new file mode 100644 index 00000000..8f17b0cc --- /dev/null +++ b/src/addons/terminado/Interfaces.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. + */ + +import { Terminal } from 'xterm'; + +export interface ITerminadoAddonTerminal extends Terminal { + __socket?: WebSocket; + __attachSocketBuffer?: string; + + __getMessage?(ev: MessageEvent): void; + __flushBuffer?(): void; + __pushToBuffer?(data: string): void; + __sendData?(data: string): void; + __setSize?(size: {rows: number, cols: number}): void; +} diff --git a/src/addons/terminado/terminado.test.ts b/src/addons/terminado/terminado.test.ts index fe059898..2e4a53c5 100644 --- a/src/addons/terminado/terminado.test.ts +++ b/src/addons/terminado/terminado.test.ts @@ -5,14 +5,14 @@ import { assert, expect } from 'chai'; -import * as terminado from './terminado' +import * as terminado from './terminado'; class MockTerminal {} describe('terminado addon', () => { describe('apply', () => { it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => { - terminado.apply(MockTerminal); + terminado.apply(MockTerminal); assert.equal(typeof (MockTerminal).prototype.terminadoAttach, 'function'); assert.equal(typeof (MockTerminal).prototype.terminadoDetach, 'function'); }); diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts index 9a7c7ad6..4acf9dd8 100644 --- a/src/addons/terminado/terminado.ts +++ b/src/addons/terminado/terminado.ts @@ -6,106 +6,107 @@ * WebSocket stream. */ +/// + +import { Terminal } from 'xterm'; +import { ITerminadoAddonTerminal } from './Interfaces'; + /** * Attaches the given terminal to the given socket. * - * @param {Terminal} term - The terminal to be attached to the given socket. - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. + * @param term The terminal to be attached to the given socket. + * @param socket The socket to attach the current terminal. + * @param bidirectional Whether the terminal should send data to the socket as well. + * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. */ -export function terminadoAttach(term, socket, bidirectional, buffered) { - bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional; - term.socket = socket; +export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { + const addonTerminal = term; + bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; + addonTerminal.__socket = socket; - term._flushBuffer = function() { - term.write(term._attachSocketBuffer); - term._attachSocketBuffer = null; + addonTerminal.__flushBuffer = () => { + addonTerminal.write(addonTerminal.__attachSocketBuffer); + addonTerminal.__attachSocketBuffer = null; }; - term._pushToBuffer = function(data) { - if (term._attachSocketBuffer) { - term._attachSocketBuffer += data; + addonTerminal.__pushToBuffer = (data: string) => { + if (addonTerminal.__attachSocketBuffer) { + addonTerminal.__attachSocketBuffer += data; } else { - term._attachSocketBuffer = data; - setTimeout(term._flushBuffer, 10); + addonTerminal.__attachSocketBuffer = data; + setTimeout(addonTerminal.__flushBuffer, 10); } }; - term._getMessage = function(ev) { - var data = JSON.parse(ev.data) - if( data[0] == "stdout" ) { + addonTerminal.__getMessage = (ev: MessageEvent) => { + const data = JSON.parse(ev.data); + if (data[0] === 'stdout') { if (buffered) { - term._pushToBuffer(data[1]); + addonTerminal.__pushToBuffer(data[1]); } else { - term.write(data[1]); + addonTerminal.write(data[1]); } } }; - term._sendData = function(data) { + addonTerminal.__sendData = (data: string) => { socket.send(JSON.stringify(['stdin', data])); }; - term._setSize = function(size) { + addonTerminal.__setSize = (size: {rows: number, cols: number}) => { socket.send(JSON.stringify(['set_size', size.rows, size.cols])); }; - socket.addEventListener('message', term._getMessage); + socket.addEventListener('message', addonTerminal.__getMessage); if (bidirectional) { - term.on('data', term._sendData); + addonTerminal.on('data', addonTerminal.__sendData); } - term.on('resize', term._setSize); + addonTerminal.on('resize', addonTerminal.__setSize); - socket.addEventListener('close', term.terminadoDetach.bind(term, socket)); - socket.addEventListener('error', term.terminadoDetach.bind(term, socket)); -}; + socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket)); + socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket)); +} /** * Detaches the given terminal from the given socket * - * @param {Xterm} term - The terminal to be detached from the given socket. - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. + * @param term The terminal to be detached from the given socket. + * @param socket The socket from which to detach the current terminal. */ -export function terminadoDetach(term, socket) { - term.off('data', term._sendData); +export function terminadoDetach(term: Terminal, socket: WebSocket): void { + const addonTerminal = term; + addonTerminal.off('data', addonTerminal.__sendData); - socket = (typeof socket == 'undefined') ? term.socket : socket; + socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; if (socket) { - socket.removeEventListener('message', term._getMessage); + socket.removeEventListener('message', addonTerminal.__getMessage); } - delete term.socket; -}; + delete addonTerminal.__socket; +} -export function apply(terminalConstructor) { +export function apply(terminalConstructor: typeof Terminal): void { /** * Attaches the current terminal to the given socket * - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. + * @param socket - The socket to attach the current terminal. + * @param bidirectional - Whether the terminal should send data to the socket as well. + * @param buffered - Whether the rendering of incoming data should happen instantly or at a + * maximum frequency of 1 rendering per 10ms. */ - terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) { + (terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { return terminadoAttach(this, socket, bidirectional, buffered); }; /** * Detaches the current terminal from the given socket. * - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. + * @param socket The socket from which to detach the current terminal. */ - terminalConstructor.prototype.terminadoDetach = function(socket) { + (terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void { return terminadoDetach(this, socket); }; } diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 775b20d5..84e3ac32 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/terminado/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/winptyCompat/Interfaces.ts b/src/addons/winptyCompat/Interfaces.ts new file mode 100644 index 00000000..8e02c64b --- /dev/null +++ b/src/addons/winptyCompat/Interfaces.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal } from 'xterm'; + +export interface IWinptyCompatAddonTerminal extends Terminal { + buffer: any; +} diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json index e15a09a8..a51f7332 100644 --- a/src/addons/winptyCompat/tsconfig.json +++ b/src/addons/winptyCompat/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/winptyCompat/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/winptyCompat/winptyCompat.test.ts b/src/addons/winptyCompat/winptyCompat.test.ts index 21e54e56..0c9269ed 100644 --- a/src/addons/winptyCompat/winptyCompat.test.ts +++ b/src/addons/winptyCompat/winptyCompat.test.ts @@ -5,14 +5,14 @@ import { assert, expect } from 'chai'; -import * as winptyCompat from './winptyCompat' +import * as winptyCompat from './winptyCompat'; class MockTerminal {} describe('winptyCompat addon', () => { describe('apply', () => { it('should do register the `winptyCompatInit` method', () => { - winptyCompat.apply(MockTerminal); + winptyCompat.apply(MockTerminal); assert.equal(typeof (MockTerminal).prototype.winptyCompatInit, 'function'); }); }); diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 723bf011..61524e12 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -3,36 +3,43 @@ * @license MIT */ -export function winptyCompatInit(terminal): void { - // Don't do anything when the platform is not Windows - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - if (!isWindows) { - return; +/// + +import { Terminal } from 'xterm'; +import { IWinptyCompatAddonTerminal } from './Interfaces'; + +export function winptyCompatInit(terminal: Terminal): void { + const addonTerminal = terminal; + + // Don't do anything when the platform is not Windows + const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; + if (!isWindows) { + return; + } + + // Winpty does not support wraparound mode which means that lines will never + // be marked as wrapped. This causes issues for things like copying a line + // retaining the wrapped new line characters or if consumers are listening + // in on the data stream. + // + // The workaround for this is to listen to every incoming line feed and mark + // the line as wrapped if the last character in the previous line is not a + // space. This is certainly not without its problems, but generally on + // Windows when text reaches the end of the terminal it's likely going to be + // wrapped. + addonTerminal.on('linefeed', () => { + const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1); + const lastChar = line[addonTerminal.cols - 1]; + + if (lastChar[3] !== 32 /* ' ' */) { + const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y); + (nextLine).isWrapped = true; } - - // Winpty does not support wraparound mode which means that lines will never - // be marked as wrapped. This causes issues for things like copying a line - // retaining the wrapped new line characters or if consumers are listening - // in on the data stream. - // - // The workaround for this is to listen to every incoming line feed and mark - // the line as wrapped if the last character in the previous line is not a - // space. This is certainly not without its problems, but generally on - // Windows when text reaches the end of the terminal it's likely going to be - // wrapped. - terminal.on('linefeed', () => { - const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); - const lastChar = line[terminal.cols - 1]; - - if (lastChar[3] !== 32 /* ' ' */) { - const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); - (nextLine).isWrapped = true; - } - }); + }); } -export function apply(terminalConstructor) { - terminalConstructor.prototype.winptyCompatInit = function(): void { +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).winptyCompatInit = function (): void { winptyCompatInit(this); }; } diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index 9ded72e5..abae1238 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -5,6 +5,7 @@ "rootDir": ".", "outDir": "../../../lib/addons/zmodem/", "sourceMap": true, - "removeComments": true + "removeComments": true, + "declaration": true } } diff --git a/src/addons/zmodem/zmodem.test.ts b/src/addons/zmodem/zmodem.test.ts index 3cb28316..682e62c8 100644 --- a/src/addons/zmodem/zmodem.test.ts +++ b/src/addons/zmodem/zmodem.test.ts @@ -5,14 +5,14 @@ import { assert, expect } from 'chai'; -import * as zmodem from './zmodem' +import * as zmodem from './zmodem'; class MockTerminal {} describe('zmodem addon', () => { describe('apply', () => { it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => { - zmodem.apply(MockTerminal); + zmodem.apply(MockTerminal); assert.equal(typeof (MockTerminal).prototype.zmodemAttach, 'function'); assert.equal(typeof (MockTerminal).prototype.zmodemBrowser, 'object'); }); diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts index 57bdde3c..e8a7824d 100644 --- a/src/addons/zmodem/zmodem.ts +++ b/src/addons/zmodem/zmodem.ts @@ -1,3 +1,12 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/// + +import { Terminal } from 'xterm'; + /** * * Allow xterm.js to handle ZMODEM uploads and downloads. @@ -27,47 +36,40 @@ * via `detach()` and a re-`attach()`.) */ -let Zmodem; +let zmodem; -export function zmodemAttach(term, ws, opts) { - if (!opts) opts = {}; +export interface IZModemOptions { + noTerminalWriteOutsideSession?: boolean; +} - var senderFunc = function _ws_sender_func(octets) { - ws.send(new Uint8Array(octets)); - }; +export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void { + const senderFunc = (octets: ArrayLike) => ws.send(new Uint8Array(octets)); - var zsentry; + let zsentry; - function _shouldWrite() { + function _shouldWrite(): boolean { return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; } - zsentry = new Zmodem.Sentry( { - to_terminal: function _to_terminal(octets) { + zsentry = new zmodem.Sentry({ + to_terminal: (octets: ArrayLike) => { if (_shouldWrite()) { term.write( String.fromCharCode.apply(String, octets) ); } }, - sender: senderFunc, + on_retract: () => (term).emit('zmodemRetract'), + on_detect: (detection: any) => (term).emit('zmodemDetect', detection) + }); - on_retract: function _on_retract() { - term.emit('zmodemRetract'); - }, + function handleWSMessage(evt: MessageEvent): void { - on_detect: function _on_detect(detection) { - term.emit('zmodemDetect', detection); - }, - } ); - - function handleWSMessage(evt) { - - //In testing with xterm.js’s demo the first message was - //always text even if the rest were binary. While that - //may be specific to xterm.js’s demo, ultimately we - //should reject anything that isn’t binary. + // In testing with xterm.js’s demo the first message was + // always text even if the rest were binary. While that + // may be specific to xterm.js’s demo, ultimately we + // should reject anything that isn’t binary. if (typeof evt.data === 'string') { if (_shouldWrite()) { term.write(evt.data); @@ -82,9 +84,9 @@ export function zmodemAttach(term, ws, opts) { ws.addEventListener('message', handleWSMessage); } -export function apply(terminalConstructor) { - Zmodem = (typeof window == 'object') ? (window).ZModem : {Browser: null}; // Nullify browser for tests +export function apply(terminalConstructor: typeof Terminal): void { + zmodem = (typeof window === 'object') ? (window).ZModem : {Browser: null}; // Nullify browser for tests - terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this); - terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser + (terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this); + (terminalConstructor.prototype).zmodemBrowser = zmodem.Browser; } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts new file mode 100644 index 00000000..c9c51cbe --- /dev/null +++ b/src/handlers/AltClickHandler.ts @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal, ICircularList, LineData } from '../Types'; +import { C0 } from '../EscapeSequences'; + +enum Direction { + Up = 'A', + Down = 'B', + Right = 'C', + Left = 'D' +} + +export class AltClickHandler { + private _startRow: number; + private _startCol: number; + private _endRow: number; + private _endCol: number; + private _lines: ICircularList; + + constructor( + private _mouseEvent: MouseEvent, + private _terminal: ITerminal + ) { + this._lines = this._terminal.buffer.lines; + this._startCol = this._terminal.buffer.x; + this._startRow = this._terminal.buffer.y; + + [this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords( + this._mouseEvent, + this._terminal.element, + this._terminal.charMeasure, + this._terminal.options.lineHeight, + this._terminal.cols, + this._terminal.rows, + false + ).map((coordinate: number) => { + return coordinate - 1; + }); + } + + /** + * Writes the escape sequences of arrows to the terminal + */ + public move(): void { + if (this._mouseEvent.altKey) { + this._terminal.send(this._arrowSequences()); + } + } + + /** + * Concatenates all the arrow sequences together. + * Resets the starting row to an unwrapped row, moves to the requested row, + * then moves to requested col. + */ + private _arrowSequences(): string { + return this._resetStartingRow() + + this._moveToRequestedRow() + + this._moveToRequestedCol(); + } + + /** + * If the initial position of the cursor is on a row that is wrapped, move the + * cursor up to the first row that is not wrapped to have accurate vertical + * positioning. + */ + private _resetStartingRow(): string { + let startRow = this._endRow - this._wrappedRowsForRow(this._endRow); + let endRow = this._endRow; + + if (this._moveToRequestedRow().length === 0) { + return ''; + } else { + return repeat(this._bufferLine( + this._startCol, this._startRow, this._startCol, + this._startRow - this._wrappedRowsForRow(this._startRow), false + ).length, this._sequence(Direction.Left)); + } + } + + /** + * Using the reset starting and ending row, move to the requested row, + * ignoring wrapped rows + */ + private _moveToRequestedRow(): string { + let startRow = this._startRow - this._wrappedRowsForRow(this._startRow); + let endRow = this._endRow - this._wrappedRowsForRow(this._endRow); + + let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(); + + return repeat(rowsToMove, this._sequence(this._verticalDirection())); + } + + /** + * Move to the requested col on the ending row + */ + private _moveToRequestedCol(): string { + let startRow; + if (this._moveToRequestedRow().length > 0) { + startRow = this._endRow - this._wrappedRowsForRow(this._endRow); + } else { + startRow = this._startRow; + } + + let endRow = this._endRow; + let direction = this._horizontalDirection(); + + return repeat(this._bufferLine( + this._startCol, startRow, this._endCol, endRow, + direction === Direction.Right + ).length, this._sequence(direction)); + } + + /** + * Utility functions + */ + + /** + * Calculates the number of wrapped rows between the unwrapped starting and + * ending rows. These rows need to ignored since the cursor skips over them. + */ + private _wrappedRowsCount(): number { + let wrappedRows = 0; + let startRow = this._startRow - this._wrappedRowsForRow(this._startRow); + let endRow = this._endRow - this._wrappedRowsForRow(this._endRow); + + for (let i = 0; i < Math.abs(startRow - endRow); i++) { + let direction = this._verticalDirection() === Direction.Up ? -1 : 1; + + if ((this._lines.get(startRow + (direction * i))).isWrapped) { + wrappedRows++; + } + } + + return wrappedRows; + } + + /** + * Calculates the number of wrapped rows that make up a given row. + * @param currentRow The row to determine how many wrapped rows make it up + */ + private _wrappedRowsForRow(currentRow: number): number { + let rowCount = 0; + let lineWraps = (this._lines.get(currentRow)).isWrapped; + + while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) { + rowCount++; + currentRow--; + lineWraps = (this._lines.get(currentRow)).isWrapped; + } + + return rowCount; + } + + /** + * Direction determiners + */ + + /** + * Determines if the right or left arrow is needed + */ + private _horizontalDirection(): Direction { + let startRow; + if (this._moveToRequestedRow().length > 0) { + startRow = this._endRow - this._wrappedRowsForRow(this._endRow); + } else { + startRow = this._startRow; + } + + if ((this._startCol < this._endCol && + startRow <= this._endRow) || // down/right or same y/right + (this._startCol >= this._endCol && + startRow < this._endRow)) { // down/left or same y/left + return Direction.Right; + } else { + return Direction.Left; + } + } + + /** + * Determines if the up or down arrow is needed + */ + private _verticalDirection(): Direction { + if (this._startRow > this._endRow) { + return Direction.Up; + } else { + return Direction.Down; + } + } + + /** + * Constructs the string of chars in the buffer from a starting row and col + * to an ending row and col + * @param startCol The starting column position + * @param startRow The starting row position + * @param endCol The ending column position + * @param endRow The ending row position + * @param forward Direction to move + */ + private _bufferLine( + startCol: number, + startRow: number, + endCol: number, + endRow: number, + forward: boolean + ): string { + let currentCol = startCol; + let currentRow = startRow; + let bufferStr = ''; + + while (currentCol !== endCol || currentRow !== endRow) { + currentCol += forward ? 1 : -1; + + if (forward && currentCol > this._terminal.cols - 1) { + bufferStr += this._terminal.buffer.translateBufferLineToString( + currentRow, false, startCol, currentCol + ); + currentCol = 0; + startCol = 0; + currentRow++; + } else if (!forward && currentCol < 0) { + bufferStr += this._terminal.buffer.translateBufferLineToString( + currentRow, false, 0, startCol + 1 + ); + currentCol = this._terminal.cols - 1; + startCol = currentCol; + currentRow--; + } + } + + return bufferStr + this._terminal.buffer.translateBufferLineToString( + currentRow, false, startCol, currentCol + ); + } + + /** + * Constructs the escape sequence for clicking an arrow + * @param direction The direction to move + */ + private _sequence(direction: Direction): string { + const mod = this._terminal.applicationCursor ? 'O' : '['; + return C0.ESC + mod + direction; + } +} + +/** + * Returns a string repeated a given number of times + * Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat + * @param count The number of times to repeat the string + * @param string The string that is to be repeated + */ +function repeat(count: number, str: string): string { + count = Math.floor(count); + let rpt = ''; + for (let i = 0; i < count; i++) { + rpt += str; + } + return rpt; +} diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index cf6661fe..7ac97714 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISelectionManager } from '../Interfaces'; +import { ITerminal, ISelectionManager } from '../Types'; interface IWindow extends Window { clipboardData?: { @@ -100,6 +100,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA textarea.focus(); // Reset the terminal textarea's styling + // Timeout needs to be long enough for click event to be handled. setTimeout(() => { textarea.style.position = null; textarea.style.width = null; @@ -107,7 +108,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA textarea.style.left = null; textarea.style.top = null; textarea.style.zIndex = null; - }, 4); + }, 200); } /** @@ -115,10 +116,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. * @param selectionManager The terminal's selection manager. + * @param shouldSelectWord If true and there is no selection the current word will be selected */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void { +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { moveTextAreaUnderMouseCursor(ev, textarea); + if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { + selectionManager.selectWordAtCursor(ev); + } + // Get textarea ready to copy from the context menu textarea.value = selectionManager.selectionText; textarea.select(); diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 8fae3171..3ab86e7c 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IMouseZoneManager, IMouseZone } from './Interfaces'; -import { ITerminal } from '../Interfaces'; +import { ITerminal } from '../Types'; +import { IMouseZoneManager, IMouseZone } from './Types'; const HOVER_DURATION = 500; @@ -151,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager { // components from handling the mouse event. const zone = this._findZoneEventAt(e); if (zone) { - // TODO: When link modifier support is added, the event should only be - // cancelled when the modifier is held (see #1021) - e.preventDefault(); - e.stopImmediatePropagation(); + if (zone.willLinkActivate(e)) { + e.preventDefault(); + e.stopImmediatePropagation(); + } } } @@ -169,7 +169,7 @@ export class MouseZoneManager implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); + const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows); if (!coords) { return null; } @@ -178,7 +178,7 @@ export class MouseZoneManager implements IMouseZoneManager { if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) { return zone; } - }; + } return null; } } @@ -189,9 +189,10 @@ export class MouseZone implements IMouseZone { public x2: number, public y: number, public clickCallback: (e: MouseEvent) => any, - public hoverCallback?: (e: MouseEvent) => any, - public tooltipCallback?: (e: MouseEvent) => any, - public leaveCallback?: () => void + public hoverCallback: (e: MouseEvent) => any, + public tooltipCallback: (e: MouseEvent) => any, + public leaveCallback: () => void, + public willLinkActivate: (e: MouseEvent) => boolean ) { } } diff --git a/src/input/Interfaces.ts b/src/input/Types.ts similarity index 62% rename from src/input/Interfaces.ts rename to src/input/Types.ts index 21514a53..f1398464 100644 --- a/src/input/Interfaces.ts +++ b/src/input/Types.ts @@ -13,7 +13,8 @@ export interface IMouseZone { x2: number; y: number; clickCallback: (e: MouseEvent) => any; - hoverCallback?: (e: MouseEvent) => any; - tooltipCallback?: (e: MouseEvent) => any; - leaveCallback?: () => any; + hoverCallback: (e: MouseEvent) => any | undefined; + tooltipCallback: (e: MouseEvent) => any | undefined; + leaveCallback: () => any | undefined; + willLinkActivate: (e: MouseEvent) => boolean; } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index b4becbce..9423f65d 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -3,10 +3,9 @@ * @license MIT */ -import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces'; -import { ITerminal, ITerminalOptions } from '../Interfaces'; +import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; +import { CharData, ITerminal, ITerminalOptions } from '../Types'; import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; -import { CharData } from '../Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; export const INVERTED_DEFAULT_COLOR = -1; @@ -25,22 +24,25 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _charAtlas: HTMLCanvasElement | ImageBitmap; constructor( - container: HTMLElement, + private _container: HTMLElement, id: string, zIndex: number, private _alpha: boolean, protected _colors: IColorSet ) { this._canvas = document.createElement('canvas'); - this._canvas.id = `xterm-${id}-layer`; + this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); - this._ctx = this._canvas.getContext('2d', {alpha: _alpha}); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + this._initCanvas(); + this._container.appendChild(this._canvas); + } + + private _initCanvas(): void { + this._ctx = this._canvas.getContext('2d', {alpha: this._alpha}); // Draw the background if this is an opaque layer - if (!_alpha) { + if (!this._alpha) { this.clearAll(); } - container.appendChild(this._canvas); } public onOptionsChanged(terminal: ITerminal): void {} @@ -54,6 +56,25 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._refreshCharAtlas(terminal, colorSet); } + protected setTransparency(terminal: ITerminal, alpha: boolean): void { + // Do nothing when alpha doesn't change + if (alpha === this._alpha) { + return; + } + + // Create new canvas and replace old one + const oldCanvas = this._canvas; + this._alpha = alpha; + // Cloning preserves properties + this._canvas = this._canvas.cloneNode(); + this._initCanvas(); + this._container.replaceChild(this._canvas, oldCanvas); + + // Regenerate char atlas and force a full redraw + this._refreshCharAtlas(terminal, this._colors); + this.onGridChanged(terminal, 0, terminal.rows - 1); + } + /** * Refreshes the char atlas, aquiring a new one if necessary. * @param terminal The terminal. @@ -64,7 +85,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { return; } this._charAtlas = null; - const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight); + const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight); if (result instanceof HTMLCanvasElement) { this._charAtlas = result; } else { @@ -201,7 +222,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param color The color of the character. */ protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { - this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + this._ctx.font = this._getFont(terminal, false); this._ctx.textBaseline = 'top'; this._clipRow(terminal, y); this._ctx.fillText( @@ -269,7 +290,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { charAtlasCellWidth, this._scaledCharHeight); } else { - this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim); + this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim); } // This draws the atlas (for debugging purposes) // this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -289,10 +310,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void { this._ctx.save(); - this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; - if (bold && terminal.options.enableBold) { - this._ctx.font = `bold ${this._ctx.font}`; - } + this._ctx.font = this._getFont(terminal, bold); this._ctx.textBaseline = 'top'; if (fg === INVERTED_DEFAULT_COLOR) { @@ -332,5 +350,16 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._scaledCellHeight); this._ctx.clip(); } + + /** + * Gets the current font. + * @param terminal The terminal. + * @param isBold If we should use the bold fontWeight. + */ + protected _getFont(terminal: ITerminal, isBold: boolean): string { + const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight; + + return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + } } diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 9ac1e161..ff8990b3 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -3,17 +3,21 @@ * @license MIT */ -import { ITerminal, ITheme } from '../Interfaces'; -import { IColorSet } from '../renderer/Interfaces'; -import { isFirefox } from '../utils/Browser'; +import { ITerminal } from '../Types'; +import { IColorSet } from './Types'; +import { isFirefox } from '../shared/utils/Browser'; +import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; export const CHAR_ATLAS_CELL_SPACING = 1; interface ICharAtlasConfig { fontSize: number; fontFamily: string; + fontWeight: string; + fontWeightBold: string; scaledCharWidth: number; scaledCharHeight: number; + allowTransparency: boolean; colors: IColorSet; } @@ -63,8 +67,29 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC } } + const canvasFactory = (width: number, height: number) => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; + }; + + const charAtlasConfig: ICharAtlasRequest = { + scaledCharWidth, + scaledCharHeight, + fontSize: terminal.options.fontSize, + fontFamily: terminal.options.fontFamily, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + background: colors.background, + foreground: colors.foreground, + ansiColors: colors.ansi, + devicePixelRatio: window.devicePixelRatio, + allowTransparency: terminal.options.allowTransparency + }; + const newEntry: ICharAtlasCacheEntry = { - bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi), + bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig), config: newConfig, ownedBy: [terminal] }; @@ -86,6 +111,9 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi scaledCharHeight, fontFamily: terminal.options.fontFamily, fontSize: terminal.options.fontSize, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + allowTransparency: terminal.options.allowTransparency, colors: clonedColors }; } @@ -98,125 +126,11 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { } return a.fontFamily === b.fontFamily && a.fontSize === b.fontSize && + a.fontWeight === b.fontWeight && + a.fontWeightBold === b.fontWeightBold && + a.allowTransparency === b.allowTransparency && a.scaledCharWidth === b.scaledCharWidth && a.scaledCharHeight === b.scaledCharHeight && a.colors.foreground === b.colors.foreground && a.colors.background === b.colors.background; } - -let generator: CharAtlasGenerator; - -/** - * Initializes the char atlas generator. - * @param document The document. - */ -export function initialize(document: Document): void { - if (!generator) { - generator = new CharAtlasGenerator(document); - } -} - -class CharAtlasGenerator { - private _canvas: HTMLCanvasElement; - private _ctx: CanvasRenderingContext2D; - - constructor(private _document: Document) { - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d', {alpha: false}); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - - public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise { - const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING; - const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING; - this._canvas.width = 255 * cellWidth; - this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight; - - this._ctx.fillStyle = background; - this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); - - this._ctx.save(); - this._ctx.fillStyle = foreground; - this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; - this._ctx.textBaseline = 'top'; - - // Default color - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); - this._ctx.restore(); - } - // Default color bold - this._ctx.save(); - this._ctx.font = `bold ${this._ctx.font}`; - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); - this._ctx.restore(); - } - this._ctx.restore(); - - // Colors 0-15 - this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`; - for (let colorIndex = 0; colorIndex < 16; colorIndex++) { - // colors 8-15 are bold - if (colorIndex === 8) { - this._ctx.font = `bold ${this._ctx.font}`; - } - const y = (colorIndex + 2) * cellHeight; - // Draw ascii characters - for (let i = 0; i < 256; i++) { - this._ctx.save(); - this._ctx.beginPath(); - this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight); - this._ctx.clip(); - this._ctx.fillStyle = ansiColors[colorIndex]; - this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y); - this._ctx.restore(); - } - } - this._ctx.restore(); - - // Support is patchy for createImageBitmap at the moment, pass a canvas back - // if support is lacking as drawImage works there too. Firefox is also - // included here as ImageBitmap appears both buggy and has horrible - // performance (tested on v55). - if (!('createImageBitmap' in window) || isFirefox) { - // Regenerate canvas and context as they are now owned by the char atlas - const result = this._canvas; - this._canvas = this._document.createElement('canvas'); - this._ctx = this._canvas.getContext('2d'); - this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - return result; - } - - const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - - // Remove the background color from the image so characters may overlap - const r = parseInt(background.substr(1, 2), 16); - const g = parseInt(background.substr(3, 2), 16); - const b = parseInt(background.substr(5, 2), 16); - this._clearColor(charAtlasImageData, r, g, b); - - const promise = window.createImageBitmap(charAtlasImageData); - // Clear the rect while the promise is in progress - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - return promise; - } - - private _clearColor(imageData: ImageData, r: number, g: number, b: number): void { - for (let offset = 0; offset < imageData.data.length; offset += 4) { - if (imageData.data[offset] === r && - imageData.data[offset + 1] === g && - imageData.data[offset + 2] === b) { - imageData.data[offset + 3] = 0; - } - } - } -} diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 6c0d2945..90ef0431 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IColorSet, IColorManager } from './Interfaces'; -import { ITheme } from '../Interfaces'; +import { IColorSet, IColorManager } from './Types'; +import { ITheme } from 'xterm'; const DEFAULT_FOREGROUND = '#ffffff'; const DEFAULT_BACKGROUND = '#000000'; @@ -86,7 +86,7 @@ export class ColorManager implements IColorManager { */ public setTheme(theme: ITheme): void { this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; - this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND); + this.colors.background = theme.background || DEFAULT_BACKGROUND; this.colors.cursor = theme.cursor || DEFAULT_CURSOR; this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT; this.colors.selection = theme.selection || DEFAULT_SELECTION; @@ -107,20 +107,4 @@ export class ColorManager implements IColorManager { this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14]; this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15]; } - - private _validateColor(color: string, fallback: string): string { - if (!color) { - return fallback; - } - if (color.length === 7 && color.charAt(0) === '#') { - return color; - } - if (color.length === 4 && color.charAt(0) === '#') { - const r = color.charAt(1); - const g = color.charAt(2); - const b = color.charAt(3); - return `#${r}${r}${g}${g}${b}${b}`; - } - return fallback; - } } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 689d6914..d430b81d 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,15 +3,13 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CharData } from '../Types'; +import { CharData, IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; -interface CursorState { +interface ICursorState { x: number; y: number; isFocused: boolean; @@ -25,7 +23,7 @@ interface CursorState { const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { - private _state: CursorState; + private _state: ICursorState; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; private _isFocused: boolean; @@ -108,9 +106,10 @@ export class CursorRenderLayer extends BaseRenderLayer { } public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void { - // Only render if the animation frame is not active if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) { this._render(terminal, false); + } else { + this._cursorBlinkStateManager.restartBlinkAnimation(terminal); } } diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts deleted file mode 100644 index be1b39dd..00000000 --- a/src/renderer/Interfaces.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces'; - -export interface IRenderer extends IEventEmitter { - dimensions: IRenderDimensions; - colorManager: IColorManager; - - setTheme(theme: ITheme): IColorSet; - onWindowResize(devicePixelRatio: number): void; - onResize(cols: number, rows: number, didCharSizeChange: boolean): void; - onCharSizeChanged(): void; - onBlur(): void; - onFocus(): void; - onSelectionChanged(start: [number, number], end: [number, number]): void; - onCursorMove(): void; - onOptionsChanged(): void; - clear(): void; - queueRefresh(start: number, end: number): void; -} - -export interface IRenderLayer { - /** - * Called when the terminal loses focus. - */ - onBlur(terminal: ITerminal): void; - - /** - * * Called when the terminal gets focus. - */ - onFocus(terminal: ITerminal): void; - - /** - * Called when the cursor is moved. - */ - onCursorMove(terminal: ITerminal): void; - - /** - * Called when options change. - */ - onOptionsChanged(terminal: ITerminal): void; - - /** - * Called when the theme changes. - */ - onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; - - /** - * Called when the data in the grid has changed (or needs to be rendered - * again). - */ - onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void; - - /** - * Calls when the selection changes. - */ - onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void; - - /** - * Resize the render layer. - */ - resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; - - /** - * Clear the state of the render layer. - */ - reset(terminal: ITerminal): void; -} - -export interface IColorManager { - colors: IColorSet; -} - -export interface IColorSet { - foreground: string; - background: string; - cursor: string; - cursorAccent: string; - selection: string; - ansi: string[]; -} - -export interface IRenderDimensions { - scaledCharWidth: number; - scaledCharHeight: number; - scaledCellWidth: number; - scaledCellHeight: number; - scaledCharLeft: number; - scaledCharTop: number; - scaledCanvasWidth: number; - scaledCanvasHeight: number; - canvasWidth: number; - canvasHeight: number; - actualCellWidth: number; - actualCellHeight: number; -} diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 8f3b63a5..61352f15 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -3,21 +3,19 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces'; +import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, LinkHoverEventTypes } from '../Types'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; -import { LinkHoverEvent, LinkHoverEventTypes } from '../Types'; export class LinkRenderLayer extends BaseRenderLayer { - private _state: LinkHoverEvent = null; + private _state: ILinkHoverEvent = null; constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { super(container, 'link', zIndex, true, colors); - terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e)); - terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e)); + terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e)); + terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e)); } public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { @@ -37,13 +35,13 @@ export class LinkRenderLayer extends BaseRenderLayer { } } - private _onLinkHover(e: LinkHoverEvent): void { + private _onLinkHover(e: ILinkHoverEvent): void { this._ctx.fillStyle = this._colors.foreground; this.fillBottomLineAtCells(e.x, e.y, e.length); this._state = e; } - private _onLinkLeave(e: LinkHoverEvent): void { + private _onLinkLeave(e: ILinkHoverEvent): void { this._clearCurrentLink(); } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 31547da4..2408f808 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -3,22 +3,22 @@ * @license MIT */ -import { ITerminal, ITheme } from '../Interfaces'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; import { ColorManager } from './ColorManager'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces'; +import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types'; +import { ITerminal } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; import { EventEmitter } from '../EventEmitter'; +import { RenderDebouncer } from '../utils/RenderDebouncer'; import { ScreenDprMonitor } from '../utils/ScreenDprMonitor'; +import { ITheme } from 'xterm'; export class Renderer extends EventEmitter implements IRenderer { - /** A queue of the rows to be refreshed */ - private _refreshRowsQueue: {start: number, end: number}[] = []; - private _refreshAnimationFrame = null; + private _renderDebouncer: RenderDebouncer; private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; @@ -35,11 +35,12 @@ export class Renderer extends EventEmitter implements IRenderer { if (theme) { this.colorManager.setTheme(theme); } + this._renderLayers = [ - new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors), - new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors), - new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal), - new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors) + new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency), + new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors), + new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal), + new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors) ]; this.dimensions = { scaledCharWidth: null, @@ -57,7 +58,9 @@ export class Renderer extends EventEmitter implements IRenderer { }; this._devicePixelRatio = window.devicePixelRatio; this._updateDimensions(); + this.onOptionsChanged(); + this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); this._screenDprMonitor = new ScreenDprMonitor(); this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio)); @@ -117,6 +120,10 @@ export class Renderer extends EventEmitter implements IRenderer { this._terminal.refresh(0, this._terminal.rows - 1); } + // Resize the screen + this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth + this._terminal.viewport.scrollBarWidth}px`; + this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this.emit('resize', { width: this.dimensions.canvasWidth, height: this.dimensions.canvasHeight @@ -165,47 +172,19 @@ export class Renderer extends EventEmitter implements IRenderer { * @param {number} start The start row. * @param {number} end The end row. */ - public queueRefresh(start: number, end: number): void { + public refreshRows(start: number, end: number): void { if (this._isPaused) { this._needsFullRefresh = true; return; } - this._refreshRowsQueue.push({ start: start, end: end }); - if (!this._refreshAnimationFrame) { - this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); - } + this._renderDebouncer.refresh(start, end); } /** * Performs the refresh loop callback, calling refresh only if a refresh is * necessary before queueing up the next one. */ - private _refreshLoop(): void { - let start; - let end; - if (this._refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this._terminal.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this._refreshRowsQueue[0].start; - end = this._refreshRowsQueue[0].end; - for (let i = 1; i < this._refreshRowsQueue.length; i++) { - if (this._refreshRowsQueue[i].start < start) { - start = this._refreshRowsQueue[i].start; - } - if (this._refreshRowsQueue[i].end > end) { - end = this._refreshRowsQueue[i].end; - } - } - } - this._refreshRowsQueue = []; - this._refreshAnimationFrame = null; - - // Render - start = Math.max(start, 0); - end = Math.min(end, this._terminal.rows - 1); + private _renderRows(start: number, end: number): void { this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._terminal.emit('refresh', {start, end}); } @@ -268,7 +247,5 @@ export class Renderer extends EventEmitter implements IRenderer { // differ. this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; - } - } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 9740bb9a..53fc9b39 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -3,11 +3,10 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; +import { IBuffer, ICharMeasure, ITerminal } from '../Types'; import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; export class SelectionRenderLayer extends BaseRenderLayer { diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 2ecb6ec6..a2ab9f19 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,12 +3,10 @@ * @license MIT */ -import { IColorSet, IRenderDimensions } from './Interfaces'; -import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces'; import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; -import { FLAGS } from './Types'; +import { FLAGS, IColorSet, IRenderDimensions } from './Types'; +import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types'; import { GridCache } from './GridCache'; -import { CharData } from '../Types'; import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; /** @@ -24,8 +22,8 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterFont: string; private _characterOverlapCache: { [key: string]: boolean } = {}; - constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { - super(container, 'text', zIndex, false, colors); + constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) { + super(container, 'text', zIndex, alpha, colors); this._state = new GridCache(); } @@ -33,7 +31,7 @@ export class TextRenderLayer extends BaseRenderLayer { super.resize(terminal, dim, charSizeChanged); // Clear the character width cache if the font or width has changed - const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`; + const terminalFont = this._getFont(terminal, false); if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) { this._characterWidth = dim.scaledCharWidth; this._characterFont = terminalFont; @@ -166,7 +164,7 @@ export class TextRenderLayer extends BaseRenderLayer { this._ctx.save(); if (flags & FLAGS.BOLD) { - this._ctx.font = `bold ${this._ctx.font}`; + this._ctx.font = this._getFont(terminal, true); // Convert the FG color to the bold variant if (fg < 8) { fg += 8; @@ -192,9 +190,13 @@ export class TextRenderLayer extends BaseRenderLayer { } } - /** - * Whether a character is overlapping to the next cell. - */ + public onOptionsChanged(terminal: ITerminal): void { + this.setTransparency(terminal, terminal.options.allowTransparency); + } + + /** + * Whether a character is overlapping to the next cell. + */ private _isOverlapping(charData: CharData): boolean { // Only single cell characters can be overlapping, rendering issues can // occur without this check diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 31705a3f..6f6e5f0e 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -3,7 +3,10 @@ * @license MIT */ - /** +import { ITerminal } from '../Types'; +import { IEventEmitter, ITheme } from 'xterm'; + +/** * Flags used to render terminal text properly. */ export enum FLAGS { @@ -13,4 +16,97 @@ export enum FLAGS { INVERSE = 8, INVISIBLE = 16, DIM = 32 -}; +} + +export interface IRenderer extends IEventEmitter { + dimensions: IRenderDimensions; + colorManager: IColorManager; + + setTheme(theme: ITheme): IColorSet; + onWindowResize(devicePixelRatio: number): void; + onResize(cols: number, rows: number, didCharSizeChange: boolean): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number], end: [number, number]): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + refreshRows(start: number, end: number): void; +} + +export interface IColorManager { + colors: IColorSet; +} + +export interface IColorSet { + foreground: string; + background: string; + cursor: string; + cursorAccent: string; + selection: string; + ansi: string[]; +} + +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +export interface IRenderLayer { + /** + * Called when the terminal loses focus. + */ + onBlur(terminal: ITerminal): void; + + /** + * * Called when the terminal gets focus. + */ + onFocus(terminal: ITerminal): void; + + /** + * Called when the cursor is moved. + */ + onCursorMove(terminal: ITerminal): void; + + /** + * Called when options change. + */ + onOptionsChanged(terminal: ITerminal): void; + + /** + * Called when the theme changes. + */ + onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void; + + /** + * Called when the data in the grid has changed (or needs to be rendered + * again). + */ + onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void; + + /** + * Calls when the selection changes. + */ + onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void; + + /** + * Resize the render layer. + */ + resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; + + /** + * Clear the state of the render layer. + */ + reset(terminal: ITerminal): void; +} diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/CharAtlasGenerator.ts new file mode 100644 index 00000000..96cf6cc6 --- /dev/null +++ b/src/shared/CharAtlasGenerator.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight } from 'xterm'; +import { isFirefox } from './utils/Browser'; + +declare const Promise: any; + +export interface IOffscreenCanvas { + width: number; + height: number; + getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D; + transferToImageBitmap(): ImageBitmap; +} + +export interface ICharAtlasRequest { + scaledCharWidth: number; + scaledCharHeight: number; + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + background: string; + foreground: string; + ansiColors: string[]; + devicePixelRatio: number; + allowTransparency: boolean; +} + +export const CHAR_ATLAS_CELL_SPACING = 1; + +/** + * Generates a char atlas. + * @param context The window or worker context. + * @param canvasFactory A function to generate a canvas with a width or height. + * @param request The config for the new char atlas. + */ +export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise { + const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; + const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; + const canvas = canvasFactory( + /*255 ascii chars*/255 * cellWidth, + (/*default+default bold*/2 + /*0-15*/16) * cellHeight + ); + const ctx = canvas.getContext('2d', {alpha: request.allowTransparency}); + + ctx.fillStyle = request.background; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.fillStyle = request.foreground; + ctx.font = getFont(request.fontWeight, request); + ctx.textBaseline = 'top'; + + // Default color + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.restore(); + } + // Default color bold + ctx.save(); + ctx.font = getFont(request.fontWeightBold, request); + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); + ctx.clip(); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.restore(); + } + ctx.restore(); + + // Colors 0-15 + ctx.font = getFont(request.fontWeight, request); + for (let colorIndex = 0; colorIndex < 16; colorIndex++) { + // colors 8-15 are bold + if (colorIndex === 8) { + ctx.font = getFont(request.fontWeightBold, request); + } + const y = (colorIndex + 2) * cellHeight; + // Draw ascii characters + for (let i = 0; i < 256; i++) { + ctx.save(); + ctx.beginPath(); + ctx.rect(i * cellWidth, y, cellWidth, cellHeight); + ctx.clip(); + ctx.fillStyle = request.ansiColors[colorIndex]; + ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.restore(); + } + } + ctx.restore(); + + // Support is patchy for createImageBitmap at the moment, pass a canvas back + // if support is lacking as drawImage works there too. Firefox is also + // included here as ImageBitmap appears both buggy and has horrible + // performance (tested on v55). + if (!('createImageBitmap' in context) || isFirefox) { + // Don't attempt to clear background colors if createImageBitmap is not supported + if (canvas instanceof HTMLCanvasElement) { + // Just return the HTMLCanvas if it's a HTMLCanvasElement + return canvas; + } else { + // Transfer to an ImageBitmap is this is an OffscreenCanvas + return new Promise(r => r(canvas.transferToImageBitmap())); + } + } + + const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // Remove the background color from the image so characters may overlap + const r = parseInt(request.background.substr(1, 2), 16); + const g = parseInt(request.background.substr(3, 2), 16); + const b = parseInt(request.background.substr(5, 2), 16); + clearColor(charAtlasImageData, r, g, b); + + return context.createImageBitmap(charAtlasImageData); +} + +/** + * Makes a partiicular rgb color in an ImageData completely transparent. + */ +function clearColor(imageData: ImageData, r: number, g: number, b: number): void { + for (let offset = 0; offset < imageData.data.length; offset += 4) { + if (imageData.data[offset] === r && + imageData.data[offset + 1] === g && + imageData.data[offset + 2] === b) { + imageData.data[offset + 3] = 0; + } + } +} + +function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string { + return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`; +} diff --git a/src/utils/Browser.ts b/src/shared/utils/Browser.ts similarity index 76% rename from src/utils/Browser.ts rename to src/shared/utils/Browser.ts index 48c0c374..be71d875 100644 --- a/src/utils/Browser.ts +++ b/src/shared/utils/Browser.ts @@ -3,8 +3,6 @@ * @license MIT */ -import { contains } from './Generic'; - const isNode = (typeof navigator === 'undefined') ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; @@ -20,3 +18,12 @@ export const isIpad = platform === 'iPad'; export const isIphone = platform === 'iPhone'; export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); export const isLinux = platform.indexOf('Linux') >= 0; + +/** + * Return if the given array contains the given element + * @param {Array} array The array to search for the given element. + * @param {Object} el The element to look for into the array + */ +function contains(arr: any[], el: any): boolean { + return arr.indexOf(el) >= 0; +} diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index f50ce96b..e4f4e1d6 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -4,8 +4,8 @@ */ import jsdom = require('jsdom'); +import { ICharMeasure, ITerminal } from '../Types'; import { assert } from 'chai'; -import { ICharMeasure, ITerminal } from '../Interfaces'; import { CharMeasure } from './CharMeasure'; describe('CharMeasure', () => { diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 62291ab2..91cfce5f 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -3,8 +3,8 @@ * @license MIT */ +import { ICharMeasure, ITerminal, ITerminalOptions } from '../Types'; import { EventEmitter } from '../EventEmitter'; -import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces'; /** * Utility class that measures the size of a character. Measurements are done in diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 97a32b79..6b74971b 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -4,7 +4,7 @@ */ import { EventEmitter } from '../EventEmitter'; -import { ICircularList } from '../Interfaces'; +import { ICircularList } from '../Types'; /** * Represents a circular list; a list with a maximum size that wraps around when push is called, diff --git a/src/utils/Dom.ts b/src/utils/Dom.ts new file mode 100644 index 00000000..889c88c2 --- /dev/null +++ b/src/utils/Dom.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'xterm'; + +/** + * Adds a disposabe listener to a node in the DOM, returning the disposable. + * @param type The event type. + * @param handler The handler for the listener. + */ +export function addDisposableListener( + node: Element | Window | Document, + type: string, + handler: (e: any) => void, + useCapture?: boolean +): IDisposable { + node.addEventListener(type, handler, useCapture); + return { + dispose: () => { + if (!handler) { + // Already disposed + return; + } + node.removeEventListener(type, handler, useCapture); + node = null; + handler = null; + } + }; +} diff --git a/src/utils/Generic.ts b/src/utils/Generic.ts deleted file mode 100644 index 82373617..00000000 --- a/src/utils/Generic.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -/** - * Return if the given array contains the given element - * @param {Array} array The array to search for the given element. - * @param {Object} el The element to look for into the array - */ -export function contains(arr: any[], el: any): boolean { - return arr.indexOf(el) >= 0; -}; diff --git a/src/utils/MouseHelper.ts b/src/utils/MouseHelper.ts index d7d8f698..f62593eb 100644 --- a/src/utils/MouseHelper.ts +++ b/src/utils/MouseHelper.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { ICharMeasure } from '../Interfaces'; -import { IRenderer } from '../renderer/Interfaces'; +import { ICharMeasure } from '../Types'; +import { IRenderer } from '../renderer/Types'; export class MouseHelper { constructor(private _renderer: IRenderer) {} @@ -24,7 +24,7 @@ export class MouseHelper { while (element) { x -= element.offsetLeft; y -= element.offsetTop; - element = 'offsetParent' in element ? element.offsetParent : element.parentElement; + element = element.offsetParent; } element = originalElement; while (element && element !== element.ownerDocument.body) { diff --git a/src/utils/RenderDebouncer.ts b/src/utils/RenderDebouncer.ts new file mode 100644 index 00000000..ab88fb5f --- /dev/null +++ b/src/utils/RenderDebouncer.ts @@ -0,0 +1,51 @@ +import { ITerminal } from '../Types'; +import { IDisposable } from 'xterm'; + +/** + * Debounces calls to render terminal rows using animation frames. + */ +export class RenderDebouncer implements IDisposable { + private _rowStart: number; + private _rowEnd: number; + private _animationFrame: number = null; + + constructor( + private _terminal: ITerminal, + private _callback: (start: number, end: number) => void + ) { + } + + public dispose(): void { + if (this._animationFrame) { + window.cancelAnimationFrame(this._animationFrame); + this._animationFrame = null; + } + } + + public refresh(rowStart?: number, rowEnd?: number): void { + rowStart = rowStart || 0; + rowEnd = rowEnd || this._terminal.rows - 1; + this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart; + this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd; + + if (this._animationFrame) { + return; + } + + this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + } + + private _innerRefresh(): void { + // Clamp values + this._rowStart = Math.max(this._rowStart, 0); + this._rowEnd = Math.min(this._rowEnd, this._terminal.rows - 1); + + // Run render callback + this._callback(this._rowStart, this._rowEnd); + + // Reset debouncer + this._rowStart = null; + this._rowEnd = null; + this._animationFrame = null; + } +} diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 80e25277..8ba16179 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -3,13 +3,77 @@ * @license MIT */ -import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces'; -import { LineData } from '../Types'; +import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; +import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types'; import { Buffer } from '../Buffer'; -import * as Browser from './Browser'; -import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; +import * as Browser from '../shared/utils/Browser'; +import { ITheme, IDisposable } from 'xterm'; export class MockTerminal implements ITerminal { + static string: any; + getOption(key: any): any { + throw new Error('Method not implemented.'); + } + setOption(key: any, value: any): void { + throw new Error('Method not implemented.'); + } + blur(): void { + throw new Error('Method not implemented.'); + } + focus(): void { + throw new Error('Method not implemented.'); + } + resize(columns: number, rows: number): void { + throw new Error('Method not implemented.'); + } + writeln(data: string): void { + throw new Error('Method not implemented.'); + } + open(parent: HTMLElement): void { + throw new Error('Method not implemented.'); + } + attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { + throw new Error('Method not implemented.'); + } + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { + throw new Error('Method not implemented.'); + } + deregisterLinkMatcher(matcherId: number): void { + throw new Error('Method not implemented.'); + } + hasSelection(): boolean { + throw new Error('Method not implemented.'); + } + getSelection(): string { + throw new Error('Method not implemented.'); + } + clearSelection(): void { + throw new Error('Method not implemented.'); + } + selectAll(): void { + throw new Error('Method not implemented.'); + } + destroy(): void { + throw new Error('Method not implemented.'); + } + scrollPages(pageCount: number): void { + throw new Error('Method not implemented.'); + } + scrollToTop(): void { + throw new Error('Method not implemented.'); + } + scrollToBottom(): void { + throw new Error('Method not implemented.'); + } + clear(): void { + throw new Error('Method not implemented.'); + } + write(data: string): void { + throw new Error('Method not implemented.'); + } + send(data: string): void { + throw new Error('Method not implemented.'); + } bracketedPasteMode: boolean; mouseHelper: IMouseHelper; renderer: IRenderer; @@ -17,6 +81,7 @@ export class MockTerminal implements ITerminal { isFocused: boolean; options: ITerminalOptions = {}; element: HTMLElement; + screenElement: HTMLElement; rowContainer: HTMLElement; selectionContainer: HTMLElement; selectionManager: ISelectionManager; @@ -33,18 +98,26 @@ export class MockTerminal implements ITerminal { scrollback: number; buffers: IBufferSet; buffer: IBuffer; + viewport: IViewport; + applicationCursor: boolean; handler(data: string): void { throw new Error('Method not implemented.'); } on(event: string, callback: () => void): void { throw new Error('Method not implemented.'); } - off(type: string, listener: IListenerType): void { + off(type: string, listener: XtermListener): void { + throw new Error('Method not implemented.'); + } + addDisposableListener(type: string, handler: XtermListener): IDisposable { throw new Error('Method not implemented.'); } scrollLines(disp: number, suppressScrollEvent: boolean): void { throw new Error('Method not implemented.'); } + scrollToRow(absoluteRow: number): number { + throw new Error('Method not implemented.'); + } cancel(ev: Event, force?: boolean): void { throw new Error('Method not implemented.'); } @@ -184,15 +257,18 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { setOption(key: string, value: any): void { this.options[key] = value; } - on(type: string, listener: IListenerType): void { + on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } - off(type: string, listener: IListenerType): void { + off(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } emit(type: string, data?: any): void { throw new Error('Method not implemented.'); } + addDisposableListener(type: string, handler: XtermListener): IDisposable { + throw new Error('Method not implemented.'); + } } export class MockBuffer implements IBuffer { @@ -220,15 +296,18 @@ export class MockBuffer implements IBuffer { export class MockRenderer implements IRenderer { colorManager: IColorManager; - on(type: string, listener: IListenerType): void { + on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } - off(type: string, listener: IListenerType): void { + off(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } emit(type: string, data?: any): void { throw new Error('Method not implemented.'); } + addDisposableListener(type: string, handler: XtermListener): IDisposable { + throw new Error('Method not implemented.'); + } dimensions: IRenderDimensions; setTheme(theme: ITheme): IColorSet { return {}; } onResize(cols: number, rows: number, didCharSizeChange: boolean): void {} @@ -240,10 +319,11 @@ export class MockRenderer implements IRenderer { onOptionsChanged(): void {} onWindowResize(devicePixelRatio: number): void {} clear(): void {} - queueRefresh(start: number, end: number): void {} + refreshRows(start: number, end: number): void {} } export class MockViewport implements IViewport { + scrollBarWidth: number = 0; onThemeChanged(colors: IColorSet): void { throw new Error('Method not implemented.'); } diff --git a/src/xterm.css b/src/xterm.css index b3ee50f0..16eb283e 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -96,9 +96,18 @@ background-color: #000; overflow-y: scroll; cursor: default; + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; } -.xterm canvas { +.xterm .xterm-screen { + position: relative; +} + +.xterm .xterm-screen canvas { position: absolute; left: 0; top: 0; @@ -123,3 +132,26 @@ .xterm:not(.enable-mouse-events) { cursor: text; } + +.xterm .xterm-accessibility, +.xterm .xterm-message { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 100; + color: transparent; +} + +.xterm .xterm-accessibility-tree:focus [id^="xterm-active-item-"] { + outline: 1px solid #F80; +} + +.xterm .live-region { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} diff --git a/tslint.json b/tslint.json index 4d4d4aa5..37ede7fe 100644 --- a/tslint.json +++ b/tslint.json @@ -1,27 +1,38 @@ { "rules": { + "array-type": [ + true, + "array" + ], "class-name": true, "comment-format": [ true, "check-space" ], + "curly": [ + true, + "ignore-same-line" + ], "indent": [ true, "spaces" ], + "interface-name": [ + true, + "always-prefix" + ], + "interface-over-type-literal": true, "typedef": [ true, "call-signature", "parameter" ], "eofline": true, + "no-duplicate-imports": true, "no-eval": true, "no-internal-module": true, "no-trailing-whitespace": true, - "one-variable-per-declaration": [ - true, - true - ], + "one-variable-per-declaration": true, "no-unsafe-finally": true, "no-var-keyword": true, "quotemark": [ @@ -48,15 +59,21 @@ ], "variable-name": [ true, - "ban-keywords" + "ban-keywords", + "check-format", + "allow-leading-underscore" ], "whitespace": [ true, "check-branch", "check-decl", + "check-module", "check-operator", + "check-rest-spread", "check-separator", - "check-type" + "check-type", + "check-type-operator", + "check-preblock" ] } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 83199e08..dd08b55e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -8,10 +8,21 @@ */ declare module 'xterm' { + /** + * A string representing text font weight. + */ + export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + /** * An object containing start up options for the terminal. */ export interface ITerminalOptions { + /** + * Whether background should support non-opaque color. It must be set before + * executing open() method and can't be changed later without excuting it again. + * Warning: Enabling this option can reduce performances somewhat. + */ + allowTransparency?: boolean; /** * A data uri of the sound to use for the bell (needs bellStyle = 'sound'). */ @@ -44,6 +55,8 @@ declare module 'xterm' { /** * Whether to enable the rendering of bold text. + * + * @deprecated Use fontWeight and fontWeightBold instead. */ enableBold?: boolean; @@ -57,6 +70,16 @@ declare module 'xterm' { */ fontFamily?: string; + /** + * The font weight used to render non-bold text. + */ + fontWeight?: FontWeight; + + /** + * The font weight used to render bold text. + */ + fontWeightBold?: FontWeight; + /** * The spacing in whole pixels between characters.. */ @@ -67,11 +90,29 @@ declare module 'xterm' { */ lineHeight?: number; + /** + * Whether to treat option as the meta key. + */ + macOptionIsMeta?: boolean; + + /** + * Whether to select the word under the cursor on right click, this is + * standard behavior in a lot of macOS applications. + */ + rightClickSelectsWord?: boolean; + /** * The number of rows in the terminal. */ rows?: number; + /** + * Whether screen reader support is enabled. When on this will expose + * supporting elements in the DOM to support NVDA on Windows and VoiceOver + * on macOS. + */ + screenReaderMode?: boolean; + /** * The amount of scrollback in the terminal. Scrollback is the amount of rows * that are retained when lines are scrolled beyond the initial viewport. @@ -148,8 +189,8 @@ declare module 'xterm' { matchIndex?: number; /** - * A callback that validates an individual link, returning true if valid and - * false if invalid. + * A callback that validates whether to create an individual link, pass + * whether the link is valid to the callback. */ validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void; @@ -170,12 +211,40 @@ declare module 'xterm' { * default value is 0. */ priority?: number; + + /** + * A callback that fires when the mousedown and click events occur that + * determines whether a link will be activated upon click. This enables + * only activating a link when a certain modifier is held down, if not the + * mouse event will continue propagation (eg. double click to select word). + */ + willLinkActivate?: (event: MouseEvent, uri: string) => boolean; + } + + export interface IEventEmitter { + on(type: string, listener: (...args: any[]) => void): void; + off(type: string, listener: (...args: any[]) => void): void; + emit(type: string, data?: any): void; + addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; + } + + /** + * An object that can be disposed via a dispose function. + */ + export interface IDisposable { + dispose(): void; + } + + export interface ILocalizableStrings { + blankLine: string; + promptLabel: string; + tooMuchOutput: string; } /** * The class that represents an xterm.js terminal. */ - export class Terminal { + export class Terminal implements IEventEmitter { /** * The element containing the terminal. */ @@ -196,6 +265,11 @@ declare module 'xterm' { */ cols: number; + /** + * Natural language strings that can be localized. + */ + static strings: ILocalizableStrings; + /** * Creates a new `Terminal` object. * @@ -224,7 +298,7 @@ declare module 'xterm' { * @param type The type of the event. * @param listener The listener. */ - on(type: 'data', listener: (data?: string) => void): void; + on(type: 'data', listener: (...args: any[]) => void): void; /** * Registers an event listener. * @param type The type of the event. @@ -275,6 +349,10 @@ declare module 'xterm' { */ off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; + emit(type: string, data?: any): void; + + addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; + /** * Resizes the terminal. * @param x The number of columns to resize to. @@ -346,22 +424,6 @@ declare module 'xterm' { */ selectAll(): void; - // /** - // * Find the next instance of the term, then scroll to and select it. If it - // * doesn't exist, do nothing. - // * @param term Tne search term. - // * @return Whether a result was found. - // */ - // findNext(term: string): boolean; - - // /** - // * Find the previous instance of the term, then scroll to and select it. If it - // * doesn't exist, do nothing. - // * @param term Tne search term. - // * @return Whether a result was found. - // */ - // findPrevious(term: string): boolean; - /** * Destroys the terminal and detaches it from the DOM. */ @@ -404,12 +466,12 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'termName'): string; /** * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; + getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -437,6 +499,12 @@ declare module 'xterm' { * @param value The option value. */ setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void; /** * Sets an option on the terminal. * @param key The option key. @@ -454,7 +522,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; + setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; /** * Sets an option on the terminal. * @param key The option key.