├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── postinstall.js ├── rollup.config.js ├── src ├── README.md ├── clear-text-content.js ├── common │ ├── keyboard-event.js │ ├── query-selector.js │ ├── selectors.js │ └── utils.js ├── focus-document.js ├── get-active-cursor-element.js ├── get-caret-element.js ├── get-caret-word.js ├── get-caret.js ├── get-cursor-element.js ├── get-editor-element.js ├── get-line-text.js ├── get-lines-elements.js ├── get-lines-text-elements.js ├── get-lines-text.js ├── get-pages-elements.js ├── get-selection-overlay-elements.js ├── get-selection.js ├── get-text-event-target.js ├── get-word-elements.js ├── index.js ├── is-document-active.js ├── is-text-selected.js ├── move-cursor-to.js ├── mutation-observer.js ├── press-on.js ├── remove.js ├── select.js └── type-text.js ├── test └── inject.js ├── types └── index.d.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | gitignore/ 4 | types/ 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const WARN = 1; 5 | const ERROR = 2; 6 | 7 | 8 | module.exports = { 9 | root: true, 10 | extends: [ 11 | 'eslint:recommended' 12 | ], 13 | parser: 'babel-eslint', 14 | parserOptions: { 15 | ecmaVersion: 6, 16 | sourceType: 'module', 17 | ecmaFeatures: { 18 | impliedStrict: true 19 | } 20 | }, 21 | env: { 22 | browser: true, 23 | node: true 24 | }, 25 | rules: { 26 | 'no-unsafe-optional-chaining': ERROR, 27 | 'require-atomic-updates': ERROR, 28 | 'array-callback-return': ERROR, 29 | 'block-scoped-var': ERROR, 30 | 'class-methods-use-this': ERROR, 31 | curly: ERROR, 32 | 'default-case-last': ERROR, 33 | 'default-param-last': ERROR, 34 | 'dot-location': [ 35 | ERROR, 36 | 'object' 37 | ], 38 | 'dot-notation': ERROR, 39 | 'eqeqeq': [ 40 | ERROR, 41 | 'smart' 42 | ], 43 | 'grouped-accessor-pairs': ERROR, 44 | 'guard-for-in': ERROR, 45 | 'no-caller': ERROR, 46 | 'no-constructor-return': ERROR, 47 | 'no-div-regex': ERROR, 48 | 'no-eval': ERROR, 49 | 'no-extend-native': ERROR, 50 | 'no-extra-bind': ERROR, 51 | 'no-extra-label': ERROR, 52 | 'no-implied-eval': ERROR, 53 | 'no-invalid-this': ERROR, 54 | 'no-iterator': ERROR, 55 | 'no-labels': ERROR, 56 | 'no-lone-blocks': ERROR, 57 | 'no-loop-func': ERROR, 58 | 'no-multi-spaces': ERROR, 59 | 'no-new': ERROR, 60 | 'no-new-func': ERROR, 61 | 'no-new-wrappers': ERROR, 62 | 'no-octal-escape': ERROR, 63 | 'no-proto': ERROR, 64 | 'no-return-assign': ERROR, 65 | 'no-return-await': ERROR, 66 | 'no-script-url': ERROR, 67 | 'no-unmodified-loop-condition': ERROR, 68 | 'no-unused-expressions': ERROR, 69 | 'no-useless-call': ERROR, 70 | 'no-void': ERROR, 71 | 'radix': ERROR, 72 | 'require-await': ERROR, 73 | 'vars-on-top': ERROR, 74 | 'wrap-iife': ERROR, 75 | 'no-label-var': ERROR, 76 | 'no-shadow': ERROR, 77 | 'no-use-before-define': [ 78 | ERROR, 79 | 'nofunc' 80 | ], 81 | 'camelcase': ERROR, 82 | 'comma-spacing': ERROR, 83 | 'consistent-this': ERROR, 84 | 'eol-last': ERROR, 85 | 'func-call-spacing': ERROR, 86 | 'linebreak-style': ERROR, 87 | 'max-len': WARN, 88 | 'new-parens': ERROR, 89 | 'no-array-constructor': ERROR, 90 | 'no-multi-assign': ERROR, 91 | 'no-multiple-empty-lines': ERROR, 92 | 'no-tabs': ERROR, 93 | 'no-trailing-spaces': ERROR, 94 | 'no-whitespace-before-property': ERROR, 95 | 'semi': ERROR, 96 | 'generator-star-spacing': [ 97 | ERROR, 98 | 'after' 99 | ], 100 | 'no-confusing-arrow': ERROR, 101 | 'no-duplicate-imports': ERROR, 102 | 'no-useless-computed-key': ERROR, 103 | 'no-useless-rename': ERROR, 104 | 'no-var': ERROR, 105 | 'prefer-arrow-callback': ERROR, 106 | 'prefer-const': ERROR, 107 | 'prefer-rest-params': ERROR 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | gitignore 4 | 5 | *.log 6 | *.pem 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.3.0 (June 1, 2021) 2 | 3 | - Add `addEventListener` which supports `selectionchange` event. [#9](https://github.com/Amaimersion/google-docs-utils/issues/9) 4 | - Add `isDocumentActive`: check if document is focused and active. 5 | - Add `focusDocument`: make document focused and active. 6 | - Add `remove` namespace which includes `PrevWord`, `NextWord`, `Selection` methods. Using these methods you can remove document objects. 7 | - Add `moveCursorTo` namespace which includes `PrevCharacter`, `NextCharacter`, `PrevLine`, `NextLine`, `PrevWord`, `NextWord`, `PrevParagraph`, `NextParagraph`, `LineStart`, `LineEnd`, `DocumentStart`, `DocumentEnd` methods. Using these methods you can move cursor over the document. 8 | - Add `select` namespace which includes `All`, `PrevCharacter`, `NextCharacter`, `PrevWord`, `NextWord`, `PrevLine`, `NextLine`, `PrevParagraph`, `NextParagraph`, `TextBetweenCursorAndLineStart`, `TextBetweenCursorAndLineEnd`, `TextBetweenCursorAndDocumentStart`, `TextBetweenCursorAndDocumentEnd` methods. Using these methods you can select text content in document. [#3](https://github.com/Amaimersion/google-docs-utils/issues/3) 9 | - `deleteSelection`: **BREAKING CHANGES:** moved into `remove.Selection`. 10 | - `pressOn`: **BREAKING CHANGES:** `SelectAll` moved to `select.All`. Added: `Home`, `End`, `Bold`, `Italic`, `Underline`. `Character`, `Delete`, `Backspace`, `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown` can accept optional modificator flag (Ctrl or Shift). 11 | 12 | 13 | # 2.2.1 (May 28, 2021) 14 | 15 | - **WARNING**: this library may no longer work after July 2021. See `README` for more. [#10](https://github.com/Amaimersion/google-docs-utils/issues/10) 16 | 17 | 18 | # 2.2.0 (April 16, 2021) 19 | 20 | - `getWordElements()`: fixed a bug when text of line with different formatting not handled correctly. **BREAKING CHANGES:** now it will return array of arrays. See documentation for more. [#4](https://github.com/Amaimersion/google-docs-utils/issues/4) 21 | - `getSelection()`: fixed a bug when text of line with different formatting not handled correctly. **BREAKING CHANGES:** now it will return array of arrays. See documentation for more. 22 | - `getCaret()`: **BREAKING CHANGES:** `positionIndex` renamed to `positionIndexRelativeToWord`. See documentation for more. 23 | - `getCaretWord()`: fixed a bug when this method not worked with other languages but English. **WARNING:** it still not work with languages which doesn't have upper and lower symbols (Chinese, Japanese, Arabic, Hebrew, etc.). 24 | - `getLinesTextElements()`, `getLinesText()`, `getCaret()`: fixed a bug when text of line with different formatting not handled correctly. 25 | - Added documentation about known limitations. 26 | 27 | 28 | # 2.1.2 (April 2, 2021) 29 | 30 | - Improve documentation for `getTextEventTarget()` method. 31 | 32 | 33 | # 2.1.1 (December 29, 2020) 34 | 35 | - Add `pressOn.PrintDialog`: call print dialog. 36 | 37 | 38 | # 2.1.0 (December 28, 2020) 39 | 40 | - Add `pressOn` namespace which includes `Character`, `Space`, `Delete`, `Backspace`, `Enter`, `Tab`, `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`, `Undo`, `Redo`, `SelectAll` methods. Using these methods you can imitate physical key presses. 41 | - Add `typeText`: type text at current caret position. 42 | - Add `isTextSelected`: indicates what text is selected (at least on one line). 43 | - Add `deleteSelection`: remove current selection. 44 | - Add type definitions. 45 | 46 | 47 | # 2.0.0 (December 24, 2020) 48 | 49 | Complete refactoring of old code. Now this library can be used as both IIFE and CJS. Starting from `2.0.0` version the code have nothing common with `1.x.x` versions, excepts some core concepts. 50 | 51 | 52 | # 1.1.0 (December 7, 2020) 53 | 54 | - Remove `selectedText`. 55 | - Add `selectionText`: array (mapped to lines count) of selected text. 56 | - Add `selectionRect`: array (mapped to lines count) of `DOMRect` of selected text. 57 | - Add `selectionNode`: array (mapped to lines count) of `HTMLElement` of selected text. 58 | 59 | 60 | # 1.0.0 (December 7, 2020) 61 | 62 | Forked from [JensPLarsen/ChromeExtension-GoogleDocsUtil](https://github.com/JensPLarsen/ChromeExtension-GoogleDocsUtil). 63 | 64 | - Added NPM support. 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sergey Kuznetsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING: this project may no longer work after July 2021 2 | 3 | Google Docs plans to switch to canvas based rendering instead of HTML based rendering. Expected date is around the end of July 2021. 4 | 5 | This library relies on HTML based rendering. It is means that all existing functionality will stop working with new canvas based rendering. Highly unlikely that all existing functionality will be adopted to canvas based rendering. 6 | 7 | See [#10](https://github.com/Amaimersion/google-docs-utils/issues/10) for more. 8 | 9 | See [this](https://github.com/Amaimersion/google-docs-utils/issues/10#issuecomment-956008973) for temporary workaround. 10 | 11 | 12 | # google-docs-utils 13 | 14 | Utilities for interaction with Google Docs using JavaScript. 15 | 16 | 17 | ## Content 18 | - [Content](#content) 19 | - [What for?](#what-for) 20 | - [Installation](#installation) 21 | - [Node.js](#nodejs) 22 | - [Browser](#browser) 23 | - [Usage](#usage) 24 | - [Node.js](#nodejs-1) 25 | - [Browser](#browser-1) 26 | - [API](#api) 27 | - [getEditorElement](#geteditorelement) 28 | - [getPagesElements](#getpageselements) 29 | - [getLinesElements](#getlineselements) 30 | - [getLinesTextElements](#getlinestextelements) 31 | - [getLinesText](#getlinestext) 32 | - [getLineText](#getlinetext) 33 | - [getWordElements](#getwordelements) 34 | - [getSelectionOverlayElements](#getselectionoverlayelements) 35 | - [getSelection](#getselection) 36 | - [getCursorElement](#getcursorelement) 37 | - [getActiveCursorElement](#getactivecursorelement) 38 | - [getCaretElement](#getcaretelement) 39 | - [getCaret](#getcaret) 40 | - [getCaretWord](#getcaretword) 41 | - [getTextEventTarget](#gettexteventtarget) 42 | - [clearTextContent](#cleartextcontent) 43 | - [addEventListener](#addeventlistener) 44 | - [selectionchange](#selectionchange) 45 | - [pressOn](#presson) 46 | - [Character](#character) 47 | - [Space](#space) 48 | - [Delete](#delete) 49 | - [Backspace](#backspace) 50 | - [Enter](#enter) 51 | - [Tab](#tab) 52 | - [ArrowLeft](#arrowleft) 53 | - [ArrowRight](#arrowright) 54 | - [ArrowUp](#arrowup) 55 | - [ArrowDown](#arrowdown) 56 | - [Home](#home) 57 | - [End](#end) 58 | - [Undo](#undo) 59 | - [Redo](#redo) 60 | - [Bold](#bold) 61 | - [Italic](#italic) 62 | - [Underline](#underline) 63 | - [PrintDialog](#printdialog) 64 | - [typeText](#typetext) 65 | - [isTextSelected](#istextselected) 66 | - [isDocumentActive](#isdocumentactive) 67 | - [focusDocument](#focusdocument) 68 | - [remove](#remove) 69 | - [PrevWord](#prevword) 70 | - [NextWord](#nextword) 71 | - [Selection](#selection) 72 | - [moveCursorTo](#movecursorto) 73 | - [PrevCharacter](#prevcharacter) 74 | - [NextCharacter](#nextcharacter) 75 | - [PrevLine](#prevline) 76 | - [NextLine](#nextline) 77 | - [PrevWord](#prevword-1) 78 | - [NextWord](#nextword-1) 79 | - [PrevParagraph](#prevparagraph) 80 | - [NextParagraph](#nextparagraph) 81 | - [LineStart](#linestart) 82 | - [LineEnd](#lineend) 83 | - [DocumentStart](#documentstart) 84 | - [DocumentEnd](#documentend) 85 | - [select](#select) 86 | - [All](#all) 87 | - [PrevCharacter](#prevcharacter-1) 88 | - [NextCharacter](#nextcharacter-1) 89 | - [PrevWord](#prevword-2) 90 | - [NextWord](#nextword-2) 91 | - [PrevLine](#prevline-1) 92 | - [NextLine](#nextline-1) 93 | - [PrevParagraph](#prevparagraph-1) 94 | - [NextParagraph](#nextparagraph-1) 95 | - [TextBetweenCursorAndLineStart](#textbetweencursorandlinestart) 96 | - [TextBetweenCursorAndLineEnd](#textbetweencursorandlineend) 97 | - [TextBetweenCursorAndDocumentStart](#textbetweencursoranddocumentstart) 98 | - [TextBetweenCursorAndDocumentEnd](#textbetweencursoranddocumentend) 99 | - [Known limitations](#known-limitations) 100 | - [Version naming](#version-naming) 101 | - [Contributing](#contributing) 102 | - [Project history](#project-history) 103 | - [License](#license) 104 | 105 | 106 | ## What for? 107 | 108 | Google Docs uses its own complex logic for displaying, storing and handling of page elements. It is good for ensuring that across many different browsers the editor is working as expected, but it makes hard to interact with document programmatically. 109 | 110 | Examples: 111 | - you can't just use `window.getSelection()` to get selected text. Google Docs creates two independent elements: one for text and one for selection overlay. Any events for normal selection will be canceled by Google Docs. 112 | - you can't just change text of element using `element.textContent = 'newText'`, because Google Docs stores current editor state internally. So, autosaving will be not triggered. Also, on further user typing, previous text will be restored while `newText` will be removed. 113 | - `element.innerText.length` will give different result than you expect because Google Docs adds special symbols (NBSP, ZWNJ) to display text correctly across different browsers. 114 | 115 | Why do you need to handle such nuances by yourself when you can just use already working solutions? So, it is what it for. 116 | 117 | 118 | ## Installation 119 | 120 | ### Node.js 121 | 122 | - with `npm`: 123 | 124 | ``` 125 | npm install google-docs-utils 126 | ``` 127 | 128 | - with `yarn`: 129 | 130 | ``` 131 | yarn add google-docs-utils 132 | ``` 133 | 134 | ### Browser 135 | 136 | Use these CDN links: 137 | 138 | - for development: 139 | 140 | ``` 141 | https://unpkg.com/google-docs-utils@latest/dist/iife/index.js 142 | ``` 143 | 144 | - for production: 145 | 146 | ``` 147 | https://unpkg.com/google-docs-utils@latest/dist/iife/index.min.js 148 | ``` 149 | 150 | Then access this library via `GoogleDocsUtils` global variable. 151 | 152 | 153 | ## Usage 154 | 155 | ### Node.js 156 | 157 | ```javascript 158 | // load all methods 159 | const GoogleDocsUtils = require('google-docs-utils'); 160 | 161 | // using ES6 162 | import * as GoogleDocsUtils from 'google-docs-utils'; 163 | 164 | // load specific methods 165 | import {getSelection} from 'google-docs-utils'; 166 | ``` 167 | 168 | ### Browser 169 | 170 | `GoogleDocsUtils` global variable will be created when you load this library. Access the methods via this variable. 171 | 172 | Example: 173 | 174 | ```javascript 175 | GoogleDocsUtils.getSelection(); 176 | ``` 177 | 178 | You can load the script using any way you like. For example, you can manually load this library through developer console: 179 | 180 | ```javascript 181 | var script = document.createElement('script'); 182 | script.type = 'text/javascript'; 183 | script.src = 'https://unpkg.com/google-docs-utils@latest/dist/iife/index.js'; 184 | document.head.appendChild(script); 185 | ``` 186 | 187 | 188 | ## API 189 | 190 | ### getEditorElement 191 | 192 | ```typescript 193 | GoogleDocsUtils.getEditorElement(): HTMLElement; 194 | ``` 195 | 196 | Returns current active editor element. You may consider it as a root element. It contains only editor itself, not control bar and other elements. 197 | 198 | ### getPagesElements 199 | 200 | ```typescript 201 | GoogleDocsUtils.getPagesElements(): HTMLElement[]; 202 | ``` 203 | 204 | Returns all rendered editor pages. 205 | 206 | ### getLinesElements 207 | 208 | ```typescript 209 | GoogleDocsUtils.getLinesElements(): HTMLElement[]; 210 | ``` 211 | 212 | Returns all lines of all rendered editor pages. Note that it also contains header lines of every page. So, `GoogleDocsUtils.getLinesElements()[0]` results to header line of first page, and `GoogleDocsUtils.getLinesElements()[1]` results to first line of first page. 213 | 214 | ### getLinesTextElements 215 | 216 | ```typescript 217 | GoogleDocsUtils.getLinesTextElements(): HTMLElement[]; 218 | ``` 219 | 220 | Returns all text elements of all rendered editor pages. Note that it also contains header text elements of every page, even if header is empty. 221 | 222 | ### getLinesText 223 | 224 | ```typescript 225 | GoogleDocsUtils.getLinesText(): string[]; 226 | ``` 227 | 228 | Returns text content of every line of all rendered pages. If line is empty, then empty string will be used as a value for that line. 229 | 230 | ### getLineText 231 | 232 | ```typescript 233 | GoogleDocsUtils.getLineText(lineIndex, [startIndex], [endIndex]): string | null; 234 | ``` 235 | 236 | Returns text of specific line. 237 | 238 | **lineIndex** 239 | 240 | - required: `true` 241 | - type: `number` 242 | 243 | Index of specific line, which starts from `0`. Note that it also points to header lines. So, for example, `0` points to header line of first page, and `1` points to first line of first page. 244 | 245 | If `lineIndex` is greater than total count of all rendered lines, then `null` will be returned instead of `string`. 246 | 247 | **startIndex** 248 | 249 | - required: `false` 250 | - type: `number` 251 | - default: `undefined` 252 | 253 | Start index for `substring()`. If not specified, then start of line is assumed. 254 | 255 | **endIndex** 256 | 257 | - required: `false` 258 | - type: `number` 259 | - default: `undefined` 260 | 261 | End index for `substring()`. If not specified, then end of line is assumed. 262 | 263 | ### getWordElements 264 | 265 | ```typescript 266 | GoogleDocsUtils.getWordElements(): Array; 267 | ``` 268 | 269 | Returns all nodes of all rendered lines which contains actual text of line. There is no point to change text of line through `textContent` or `innerText`, because these changes will be not recognized correctly. 270 | 271 | `[]` - represents line, `[][]` - represents all word nodes of that line. 272 | 273 | If text of line contains various formatting (font, bold, etc.), then it will be splitted into several word nodes. For example, "some [Arial font] text [Roboto font]" will be splitted into two nodes, "some text [Arial font]" will be represented as one node and "another [Arial font, normal] text [Arial font, bold]" will be splitted into two nodes. 274 | 275 | ### getSelectionOverlayElements 276 | 277 | ```typescript 278 | GoogleDocsUtils.getSelectionOverlayElements(): Array; 279 | ``` 280 | 281 | Returns all selection overlay elements of all rendered lines. If there are no selection for some line, then `null` will be used as a value for that line. Don't remove this element manually, because these DOM changes will be not recognized by Google Docs correctly. 282 | 283 | ### getSelection 284 | 285 | ```typescript 286 | GoogleDocsUtils.getSelection(): Array>; 287 | ``` 288 | 289 | Returns data about selection for every rendered line. Note that header line is also included in returned array. 290 | 291 | If line not selected at all, then `[]` will be equal to `null`, otherwise it will be an array that describes selection of all word nodes (see [getWordElements()](#getwordelements) documentation for more). `[][]` will be equal to `null` if that word node not part of selection, otherwise it will be an object that describes selection of that word node. 292 | 293 | **SelectionData.text** 294 | 295 | - type: `string` 296 | 297 | Original text of word node. 298 | 299 | **SelectionData.selectedText** 300 | 301 | - type: `string` 302 | 303 | Selected text. 304 | 305 | **SelectionData.selectionStart** 306 | 307 | - type: `number` 308 | 309 | Index where selection starts. It can be used for `substring()`. It is relative to word node, not entire line. 310 | 311 | **SelectionData.selectionEnd** 312 | 313 | - type: `number` 314 | 315 | Index where selection ends. It can be used for `substring()`. It is relative to word node, not entire line. 316 | 317 | **SelectionData.textElement** 318 | 319 | - type: `HTMLElement` 320 | 321 | HTML element which contains actual text. 322 | 323 | **SelectionData.selectionElement** 324 | 325 | - type: `HTMLElement` 326 | 327 | HTML element which contains selection overlay element. Every not empty `[][]` will have same `selectionElement`. 328 | 329 | **SelectionData.textRect** 330 | 331 | - type: `DOMRectReadOnly` 332 | 333 | `DOMRect` of `textElement`. 334 | 335 | **SelectionData.selectionRect** 336 | 337 | - type: `DOMRectReadOnly` 338 | 339 | `DOMRect` of `selectionElement`. Every not empty `[][]` will have same `selectionRect`. 340 | 341 | ### getCursorElement 342 | 343 | ```typescript 344 | GoogleDocsUtils.getCursorElement(): HTMLElement; 345 | ``` 346 | 347 | Returns cursor element. 348 | 349 | ### getActiveCursorElement 350 | 351 | ```typescript 352 | GoogleDocsUtils.getActiveCursorElement(): HTMLElement | null; 353 | ``` 354 | 355 | Returns active cursor element. "Active" means page is focused (cursor is blinking). `null` will be returned if cursor is not active. 356 | 357 | ### getCaretElement 358 | 359 | ```typescript 360 | GoogleDocsUtils.getCaretElement(): HTMLElement; 361 | ``` 362 | 363 | Returns caret element. 364 | 365 | ### getCaret 366 | 367 | ```typescript 368 | GoogleDocsUtils.getCaret(): CaretData; 369 | ``` 370 | 371 | Returns data about caret. 372 | 373 | **CaretData.element** 374 | 375 | - type: `HTMLElement` 376 | 377 | Caret element. 378 | 379 | **CaretData.wordElement** 380 | 381 | - type: `HTMLElement` 382 | 383 | Element which contains text of line on which caret is placed. 384 | 385 | **CaretData.lineIndex** 386 | 387 | - type: `number` 388 | 389 | Global index of line. 390 | 391 | **CaretData.positionIndexRelativeToWord** 392 | 393 | - type: `number` 394 | 395 | Before what letter caret is placed. For example, caret is placed before `w` letter in `one two three` text. `positionIndexRelativeToWord` will be equal to `5` in that case. 396 | 397 | This index relates to word node, not entire line. For example, if line contains two words with different fonts, then there will be two word nodes. 398 | 399 | ### getCaretWord 400 | 401 | ```typescript 402 | GoogleDocsUtils.getCaretWord(): CaretWordData; 403 | ``` 404 | 405 | Returns data about word on which caret is currently placed. 406 | 407 | Note that this method will not work with languages which doesn't have upper and lower symbols. For example: Chinese, Japanese, Arabic, Hebrew, etc. 408 | 409 | **CaretWordData.word** 410 | 411 | - type: `string` 412 | 413 | Full word on which caret is placed. 414 | 415 | **CaretWordData.text** 416 | 417 | - type: `string` 418 | 419 | Full text of line on which caret is placed. 420 | 421 | **CaretWordData.indexStart** 422 | 423 | - type: `number` 424 | 425 | On which index `word` starts in `text`. Can be used for `substring()`. 426 | 427 | **CaretWordData.indexEnd** 428 | 429 | - type: `number` 430 | 431 | On which index `word` ends in `text`. Can be used for `substring()`. 432 | 433 | ### getTextEventTarget 434 | 435 | ```typescript 436 | GoogleDocsUtils.getTextEventTarget(): HTMLElement | Document; 437 | ``` 438 | 439 | This element can be used to interact with text events, in particular with keyboard events (`keyup`, `keydown`, `keypress`). You can dispatch text events to that element and add event listeners to that element: 440 | 441 | - `GoogleDocsUtils.getTextEventTarget().dispatchEvent()` 442 | - `GoogleDocsUtils.getTextEventTarget().addEventListener()` 443 | 444 | You can't just interact with current `document`, because Google Docs uses separate element (`iframe` at the moment) to handle keyboard events. This element is always active (`document.activeElement`), and all text events will be handled by that element. 445 | 446 | Note that you can't interact with other events. For example, with mouse events. You also can't interact with selection events, because Google Docs implemented its own selection mechanism. Use [getSelection](#getselection) instead. 447 | 448 | ### clearTextContent 449 | 450 | ```typescript 451 | GoogleDocsUtils.clearTextContent(text): string; 452 | ``` 453 | 454 | Clears text that was extracted using `textContent` or `innerText`. It is important to handle extracted text, because it may contain special invisible symbols like `ZWNJ` or `NBSP` - these symbols will lead to unexpected result. 455 | 456 | **text** 457 | 458 | - required: `true` 459 | - type: `string` 460 | 461 | Raw text of line that was extracted using `textContent` or `innerText`. 462 | 463 | ### addEventListener 464 | 465 | ```typescript 466 | GoogleDocsUtils.addEventListener(type: string, listener: (event: GoogleDocsEvent) => any): void; 467 | ``` 468 | 469 | Sets up a function that will be called whenever the specified event will occur. 470 | 471 | **type** 472 | 473 | Case-sensitive type of event. See below documentation for all possible events. 474 | 475 | **listener** 476 | 477 | Callback function. There can be many functions for single event. Order of calling is same as order of adding. On call every function will receive event details as argument. 478 | 479 | **GoogleDocsEvent.type** 480 | 481 | The name of the event. Case-insensitive. 482 | 483 | #### selectionchange 484 | 485 | This event is fired when the current text selection on a document is changed. 486 | 487 | ### pressOn 488 | 489 | This namespace provides methods to imitate physical single key press. You can use this to interact with current editor content: clear current selection using `Delete` key, delete current character using `Backspace` key, move on new line using `Enter` key, etc. 490 | 491 | Some methods can accept on/off status of modificator keys (Ctrl, Shift, etc). Not every method support it, so, if it is present, then modificator with `true` provides different behavior than with `false`. By default all modificators are disabled. 492 | 493 | If this default typing system not suits for you, you still can implement your own typing system - just send keyboard events to [getTextEventTarget](#gettexteventtarget). 494 | 495 | This namespace provides following methods: 496 | 497 | #### Character 498 | 499 | ```typescript 500 | GoogleDocsUtils.pressOn.Character( 501 | char, 502 | { 503 | ctrlKey = false, 504 | shiftKey = false 505 | } = {} 506 | ): void; 507 | ``` 508 | 509 | **char** 510 | 511 | - required: `true` 512 | - type: `string` 513 | 514 | Single character to press on. Case sensitive. 515 | 516 | #### Space 517 | 518 | ```typescript 519 | GoogleDocsUtils.pressOn.Space(): void; 520 | ``` 521 | 522 | #### Delete 523 | 524 | ```typescript 525 | GoogleDocsUtils.pressOn.Delete( 526 | { 527 | ctrlKey = false 528 | } = {} 529 | ): void; 530 | ``` 531 | 532 | Difference between [Delete](#delete) and [Backspace](#backspace) is matters. 533 | 534 | #### Backspace 535 | 536 | ```typescript 537 | GoogleDocsUtils.pressOn.Backspace( 538 | { 539 | ctrlKey = false 540 | } = {} 541 | ): void; 542 | ``` 543 | 544 | Difference between [Delete](#delete) and [Backspace](#backspace) is matters. 545 | 546 | #### Enter 547 | 548 | ```typescript 549 | GoogleDocsUtils.pressOn.Enter(): void; 550 | ``` 551 | 552 | #### Tab 553 | 554 | ```typescript 555 | GoogleDocsUtils.pressOn.Tab(): void; 556 | ``` 557 | 558 | #### ArrowLeft 559 | 560 | ```typescript 561 | GoogleDocsUtils.pressOn.ArrowLeft( 562 | { 563 | ctrlKey = false, 564 | shiftKey = false 565 | } = {} 566 | ): void; 567 | ``` 568 | 569 | #### ArrowRight 570 | 571 | ```typescript 572 | GoogleDocsUtils.pressOn.ArrowRight( 573 | { 574 | ctrlKey = false, 575 | shiftKey = false 576 | } = {} 577 | ): void; 578 | ``` 579 | 580 | #### ArrowUp 581 | 582 | ```typescript 583 | GoogleDocsUtils.pressOn.ArrowUp( 584 | { 585 | ctrlKey = false, 586 | shiftKey = false 587 | } = {} 588 | ): void; 589 | ``` 590 | 591 | #### ArrowDown 592 | 593 | ```typescript 594 | GoogleDocsUtils.pressOn.ArrowDown( 595 | { 596 | ctrlKey = false, 597 | shiftKey = false 598 | } = {} 599 | ): void; 600 | ``` 601 | 602 | #### Home 603 | 604 | ```typescript 605 | GoogleDocsUtils.pressOn.Home( 606 | { 607 | ctrlKey = false, 608 | shiftKey = false 609 | } = {} 610 | ): void; 611 | ``` 612 | 613 | #### End 614 | 615 | ```typescript 616 | GoogleDocsUtils.pressOn.End( 617 | { 618 | ctrlKey = false, 619 | shiftKey = false 620 | } = {} 621 | ): void; 622 | ``` 623 | 624 | #### Undo 625 | 626 | ```typescript 627 | GoogleDocsUtils.pressOn.Undo(): void; 628 | ``` 629 | 630 | #### Redo 631 | 632 | ```typescript 633 | GoogleDocsUtils.pressOn.Redo(): void; 634 | ``` 635 | 636 | #### Bold 637 | 638 | ```typescript 639 | GoogleDocsUtils.pressOn.Bold(): void; 640 | ``` 641 | 642 | #### Italic 643 | 644 | ```typescript 645 | GoogleDocsUtils.pressOn.Italic(): void; 646 | ``` 647 | 648 | #### Underline 649 | 650 | ```typescript 651 | GoogleDocsUtils.pressOn.Underline(): void; 652 | ``` 653 | 654 | #### PrintDialog 655 | 656 | ```typescript 657 | GoogleDocsUtils.pressOn.PrintDialog(): void; 658 | ``` 659 | 660 | ### typeText 661 | 662 | ```typescript 663 | GoogleDocsUtils.typeText(text): void; 664 | ``` 665 | 666 | Types provided text character by character at current caret position. Imitates physical key press events. Can take a long time to type long text. Uses default [pressOn](#presson). 667 | 668 | **text** 669 | 670 | - required: `true` 671 | - type: `string` 672 | 673 | Text to type. 674 | 675 | ### isTextSelected 676 | 677 | ```typescript 678 | GoogleDocsUtils.isTextSelected(): boolean; 679 | ``` 680 | 681 | Returns status that indicates if text selection is exists on either single or multiple lines. 682 | 683 | ### isDocumentActive 684 | 685 | ```typescript 686 | GoogleDocsUtils.isDocumentActive(): boolean; 687 | ``` 688 | 689 | Returns status that indicates if document is in active state. Active state means that document is focused (cursor is blinked). 690 | 691 | ### focusDocument 692 | 693 | ```typescript 694 | GoogleDocsUtils.focusDocument(): boolean; 695 | ``` 696 | 697 | Focuses on current document. "Focus" means that document is active and available for editing: cursor is blinking or selection active. 698 | 699 | Returns `true` if there was any actions to perform a focus, otherwise `false` if document already was active and nothing was performed. 700 | 701 | ### remove 702 | 703 | This namespace provides methods to remove different document objects (text, selection, etc). 704 | 705 | #### PrevWord 706 | 707 | ```typescript 708 | GoogleDocsUtils.remove.PrevWord(): void; 709 | ``` 710 | 711 | Removes word according to the following logic: 712 | - if previous word is present, then it will be removed 713 | - else content from current line will be divided with previous line 714 | 715 | #### NextWord 716 | 717 | ```typescript 718 | GoogleDocsUtils.remove.NextWord(): void; 719 | ``` 720 | 721 | Removes word according to the following logic: 722 | - if next word is present, then it will be removed 723 | - else content from current line will be divided with next line 724 | 725 | #### Selection 726 | 727 | ```typescript 728 | GoogleDocsUtils.remove.Selection(): boolean; 729 | ``` 730 | 731 | Removes current selection. Returns `true` if selection was removed, otherwise returns `false` if nothing to remove (because nothing is selected). 732 | 733 | ### moveCursorTo 734 | 735 | This namespace provides methods to move cursor over document. 736 | 737 | #### PrevCharacter 738 | 739 | ```typescript 740 | GoogleDocsUtils.moveCursorTo.PrevCharacter(): void; 741 | ``` 742 | 743 | Moves cursor to character that is placed to the left of current cursor position. If that character placed on previous line, then previous line will be used. 744 | 745 | #### NextCharacter 746 | 747 | ```typescript 748 | GoogleDocsUtils.moveCursorTo.NextCharacter(): void; 749 | ``` 750 | 751 | Moves cursor to character that is placed to the right of current cursor position. If that character placed on next line, then next line will be used. 752 | 753 | #### PrevLine 754 | 755 | ```typescript 756 | GoogleDocsUtils.moveCursorTo.PrevLine(): void; 757 | ``` 758 | 759 | Moves cursor to the previous line and tries to keep cursor position. If there is no previous line, then moves cursor to the start of current paragraph. 760 | 761 | #### NextLine 762 | 763 | ```typescript 764 | GoogleDocsUtils.moveCursorTo.NextLine(): void; 765 | ``` 766 | 767 | Moves cursor to the next line and tries to keep cursor position. If there is no next line, then moves cursor to the end of current paragraph. 768 | 769 | #### PrevWord 770 | 771 | ```typescript 772 | GoogleDocsUtils.moveCursorTo.PrevWord(): void; 773 | ``` 774 | 775 | Moves cursor to word according to the following logic: 776 | - if it is start of current line, then to the end of previous word on previous line 777 | - else if it is start of current word, then to the start of previous word 778 | - else moves to the start of current word 779 | 780 | #### NextWord 781 | 782 | ```typescript 783 | GoogleDocsUtils.moveCursorTo.NextWord(): void; 784 | ``` 785 | 786 | Moves cursor to word according to the following logic: 787 | - if it is end of current line, then to the start of next word on next line 788 | - else if it is end of current word, then to the end of next word 789 | - else moves to the end of current word 790 | 791 | #### PrevParagraph 792 | 793 | ```typescript 794 | GoogleDocsUtils.moveCursorTo.PrevParagraph(): void; 795 | ``` 796 | 797 | Moves cursor to paragraph according to the following logic: 798 | - if it is start of current paragraph, then to the start of previous paragraph 799 | - else moves to the start of current paragraph 800 | 801 | #### NextParagraph 802 | 803 | ```typescript 804 | GoogleDocsUtils.moveCursorTo.NextParagraph(): void; 805 | ``` 806 | 807 | Moves cursor to paragraph according to the following logic: 808 | - if it is end of current paragraph, then to the end of next paragraph 809 | - else moves to the end of current paragraph 810 | 811 | #### LineStart 812 | 813 | ```typescript 814 | GoogleDocsUtils.moveCursorTo.LineStart(): void; 815 | ``` 816 | 817 | Moves cursor to the start of current line. 818 | 819 | #### LineEnd 820 | 821 | ```typescript 822 | GoogleDocsUtils.moveCursorTo.LineEnd(): void; 823 | ``` 824 | 825 | Moves cursor to the start of current line. 826 | 827 | #### DocumentStart 828 | 829 | ```typescript 830 | GoogleDocsUtils.moveCursorTo.DocumentStart(): void; 831 | ``` 832 | 833 | Moves cursor to the start of document. 834 | 835 | #### DocumentEnd 836 | 837 | ```typescript 838 | GoogleDocsUtils.moveCursorTo.DocumentEnd(): void; 839 | ``` 840 | 841 | Moves cursor to the end of document. 842 | 843 | ### select 844 | 845 | This namespace provides methods to select text content in document. 846 | 847 | #### All 848 | 849 | ```typescript 850 | GoogleDocsUtils.select.All(): void; 851 | ``` 852 | 853 | Selects text of entire document. 854 | 855 | #### PrevCharacter 856 | 857 | ```typescript 858 | GoogleDocsUtils.select.PrevCharacter(): void; 859 | ``` 860 | 861 | Selects a character that is placed to the left of current cursor position. Following logic will be used, with priority of actions from top to bottom: 862 | - if at least one character already selected with reverse selection (opposite direction), then lastly selected character will be deselected 863 | - if at least one character already selected, then next one will be selected. If that next character located on previous line, than that previous line will be used 864 | - if nothing selected, then first character will be selected 865 | 866 | #### NextCharacter 867 | 868 | ```typescript 869 | GoogleDocsUtils.select.NextCharacter(): void; 870 | ``` 871 | 872 | Selects a character that is placed to the right of current cursor position. Following logic will be used, with priority of actions from top to bottom: 873 | - if at least one character already selected with reverse selection (opposite direction), then lastly selected character will be deselected 874 | - if at least one character already selected, then next one will be selected. If that next character located on next line, than that next line will be used 875 | - if nothing selected, then first character will be selected 876 | 877 | #### PrevWord 878 | 879 | ```typescript 880 | GoogleDocsUtils.select.PrevWord(): void; 881 | ``` 882 | 883 | Same as `PrevCharacter`, but performs an action with word. 884 | 885 | #### NextWord 886 | 887 | ```typescript 888 | GoogleDocsUtils.select.NextWord(): void; 889 | ``` 890 | 891 | Same as `NextCharacter`, but performs an action with word. 892 | 893 | #### PrevLine 894 | 895 | ```typescript 896 | GoogleDocsUtils.select.PrevLine(): void; 897 | ``` 898 | 899 | Selects N number of characters to the left where N is a max length of line. 900 | 901 | #### NextLine 902 | 903 | ```typescript 904 | GoogleDocsUtils.select.NextLine(): void; 905 | ``` 906 | 907 | Same as `PrevLine`, but uses right direction. 908 | 909 | #### PrevParagraph 910 | 911 | ```typescript 912 | GoogleDocsUtils.select.PrevParagraph(): void; 913 | ``` 914 | 915 | Selects a paragraph that is placed to the left of current cursor position. Following logic will be used, with priority of actions from top to bottom: 916 | - if it is start of current paragraph, then previous paragraph will be selected 917 | - else text between current paragraph start and current cursor position will be selected 918 | 919 | #### NextParagraph 920 | 921 | ```typescript 922 | GoogleDocsUtils.select.NextParagraph(): void; 923 | ``` 924 | 925 | Selects a paragraph that is placed to the right of current cursor position. Following logic will be used, with priority of actions from top to bottom: 926 | - if it is end of current paragraph, then next paragraph will be NOT selected 927 | - else text between current paragraph end and current cursor position will be selected 928 | 929 | #### TextBetweenCursorAndLineStart 930 | 931 | ```typescript 932 | GoogleDocsUtils.select.TextBetweenCursorAndLineStart(): void; 933 | ``` 934 | 935 | Selects a text between current cursor position and current line start. 936 | 937 | #### TextBetweenCursorAndLineEnd 938 | 939 | ```typescript 940 | GoogleDocsUtils.select.TextBetweenCursorAndLineEnd(): void; 941 | ``` 942 | 943 | Same as `TextBetweenCursorAndLineStart`, but interacts with current line end. 944 | 945 | #### TextBetweenCursorAndDocumentStart 946 | 947 | ```typescript 948 | GoogleDocsUtils.select.TextBetweenCursorAndDocumentStart(): void; 949 | ``` 950 | 951 | Same as `TextBetweenCursorAndLineStart`, but interacts with document start. 952 | 953 | #### TextBetweenCursorAndDocumentEnd 954 | 955 | ```typescript 956 | GoogleDocsUtils.select.TextBetweenCursorAndDocumentEnd(): void; 957 | ``` 958 | 959 | Same as `TextBetweenCursorAndLineStart`, but interacts with document end. 960 | 961 | 962 | ## Known limitations 963 | 964 | This library may not work correctly in some conditions. It is because it still not well tested and not well developed. However, there are already some known limitations that can (but won't necessarily will) lead to problems. 965 | 966 | So, if possible, avoid these conditions: 967 | - using of non-English text. 968 | - using of various formatting (font, bold, etc.). 969 | 970 | If you experiencing some issues with these or undocumented conditions, then feel free to [create issue](https://github.com/Amaimersion/google-docs-utils/issues/new). 971 | 972 | 973 | ## Version naming 974 | 975 | This project uses following structure for version naming: `..`. 976 | 977 | 978 | ## Contributing 979 | 980 | Contributions of all sizes are welcome. Feel free! 981 | 982 | Use [issues](https://github.com/Amaimersion/google-docs-utils/issues/new) to report a bug, request a feature or ask a question. 983 | 984 | Also, consider making a [pull request](https://github.com/Amaimersion/google-docs-utils/compare) to add your own implementation of missing functionality. Big thanks for that! 985 | 986 | 987 | ## Project history 988 | 989 | Initialiy it was a fork of [JensPLarsen/ChromeExtension-GoogleDocsUtil](https://github.com/JensPLarsen/ChromeExtension-GoogleDocsUtil). Starting from 2.0.0 version the project was completely rewritten, but core concepts were keeped. 990 | 991 | 992 | ## License 993 | 994 | [MIT](https://github.com/Amaimersion/google-docs-utils/blob/master/LICENSE). 995 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | if (process.env.NODE_ENV === 'production') { 5 | module.exports = require('./dist/cjs/index.min.js'); 6 | } else { 7 | module.exports = require('./dist/cjs/index.js'); 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-docs-utils", 3 | "version": "2.3.0", 4 | "description": "Utilities for interaction with Google Docs.", 5 | "main": "index.js", 6 | "types": "./types/index.d.ts", 7 | "scripts": { 8 | "build": "rollup --config rollup.config.js", 9 | "watch": "rollup --config rollup.config.js --watch", 10 | "prepare": "yarn run build", 11 | "prepublishOnly": "yarn run lint", 12 | "postinstall": "node postinstall.js", 13 | "lint": "eslint index.js './src/**/*.js'", 14 | "lint-tests": "eslint './test/**/*.js'", 15 | "http-server": "http-server . -c-1 --ssl --cert ./cert.pem --key ./key.pem", 16 | "create-ssl": "openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem" 17 | }, 18 | "homepage": "https://github.com/Amaimersion/google-docs-utils/blob/master/README.md", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Amaimersion/google-docs-utils.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/Amaimersion/google-docs-utils/issues" 25 | }, 26 | "author": { 27 | "name": "Sergey Kuznetsov", 28 | "url": "https://github.com/Amaimersion" 29 | }, 30 | "license": "MIT", 31 | "devDependencies": { 32 | "babel-eslint": "^10.1.0", 33 | "eslint": "^7.16.0", 34 | "http-server": "^0.12.3", 35 | "rollup": "^2.35.1", 36 | "rollup-plugin-terser": "^7.0.2" 37 | }, 38 | "engines": { 39 | "node": ">=14.15.1" 40 | }, 41 | "devEngines": { 42 | "node": ">=14.15.1", 43 | "yarn": ">=1.22.5" 44 | }, 45 | "files": [ 46 | "LICENSE", 47 | "README.md", 48 | "index.js", 49 | "postinstall.js", 50 | "dist/", 51 | "types/" 52 | ], 53 | "keywords": [ 54 | "google-docs-utils", 55 | "google", 56 | "google docs", 57 | "google document", 58 | "google documents", 59 | "google utils", 60 | "google docs utils", 61 | "google document utils", 62 | "google documents utils", 63 | "google docs selection", 64 | "google docs text selection", 65 | "google docs interaction", 66 | "google documents interaction" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | let message = ( 2 | '\n' + 3 | 'google-docs-utils: ' + 4 | 'WARNING! ' + 5 | 'This library may no longer work after July 2021. ' + 6 | 'See project README for more.' + 7 | '\n' 8 | ); 9 | 10 | message = `\x1b[33m${message}\x1b[0m`; // yellow color 11 | 12 | console.warn(message); 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import {terser} from 'rollup-plugin-terser'; 2 | import {joinText} from './src/common/utils'; 3 | 4 | 5 | const BANNER = joinText( 6 | '\n', 7 | '/**', 8 | '* @license MIT', 9 | '* @see https://github.com/Amaimersion/google-docs-utils', 10 | '*/' 11 | ); 12 | const GLOBAL_NAME = 'GoogleDocsUtils'; 13 | 14 | 15 | export default [ 16 | { 17 | input: './src/index.js', 18 | output: [ 19 | { 20 | file: './dist/iife/index.js', 21 | format: 'iife', 22 | name: GLOBAL_NAME, 23 | banner: BANNER, 24 | sourcemap: 'inline' 25 | }, 26 | { 27 | file: './dist/cjs/index.js', 28 | format: 'cjs', 29 | banner: BANNER 30 | } 31 | ] 32 | }, 33 | { 34 | input: './src/index.js', 35 | output: [ 36 | { 37 | file: './dist/iife/index.min.js', 38 | format: 'iife', 39 | name: GLOBAL_NAME, 40 | banner: BANNER, 41 | plugins: [ 42 | terser() 43 | ] 44 | }, 45 | { 46 | file: './dist/cjs/index.min.js', 47 | format: 'cjs', 48 | banner: BANNER, 49 | plugins: [ 50 | terser() 51 | ] 52 | } 53 | ], 54 | watch: false 55 | } 56 | ]; 57 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## Structure 2 | 3 | - public methods that's intended for user usage are placed in this folder, 4 | - all public methods are importing through `index.js`, 5 | - `common` folder contains a code which is common for all public methods and not intended for public usage 6 | 7 | 8 | ## How to create a public method 9 | 10 | - create file in `src`, import method via `index.js`, use already created public methods and common methods, 11 | - split it into logical pieces. For example, if your method should first delete current selected content and then paste new text at some position, then you can create two separate public methods (if they not created yet), instead of creating single large file with two separate logical methods, 12 | - use methods that already was created. If already created method somehow not suits for you, then change already created method, but try to not broke old versions 13 | 14 | 15 | ## How to create a common method 16 | 17 | - if you think that you need some stuff only for internal usage, then you can create some common method, 18 | - if you sure that you need common method only for your one public method, then you probably shouldn't create common method and you should place this method in a file along with public method; however, if you can imagine that this common method can be used by some another methods, then you should create common method, 19 | - if common method is related to some logical namespace, then create/edit separate file. If you don't know where to put common method or it doesn't have it logical namespace, then put this method in `utils.js` 20 | 21 | 22 | ## Coding style 23 | 24 | - make it simple, 25 | - make it readable, 26 | - write comments 27 | -------------------------------------------------------------------------------- /src/clear-text-content.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clears a text extracted from element 3 | * using `textContent` property. 4 | * 5 | * - you may want to use this function because 6 | * Google Docs adds special symbols (ZWNJ, NBSP, etc.) 7 | * to display text correctly across all browsers. 8 | * - no sense to use this function for `innertText`. 9 | * 10 | * @param {string} textContent 11 | */ 12 | export default function clearTextContent(textContent) { 13 | textContent = removeZWNJ(textContent); 14 | textContent = removeNBSP(textContent); 15 | 16 | return textContent; 17 | } 18 | 19 | 20 | /** 21 | * Removes all ZWNJ characters. 22 | * 23 | * - https://en.wikipedia.org/wiki/Zero-width_non-joiner 24 | * 25 | * @param {string} value 26 | */ 27 | function removeZWNJ(value) { 28 | return value.replace(/\u200C/g, ''); 29 | } 30 | 31 | 32 | /** 33 | * Removes all NBSP characters. 34 | * 35 | * - https://en.wikipedia.org/wiki/Non-breaking_space 36 | * 37 | * @param {string} value 38 | */ 39 | function removeNBSP(value) { 40 | return value.replace(/\u00A0/g, ''); 41 | } 42 | -------------------------------------------------------------------------------- /src/common/keyboard-event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module can be used to imitate physical keyboard press events. 3 | * 4 | * - use `keypress` for letter characters, 5 | * - use `keydown` for special keys (ArrowLeft, Delete, etc.). 6 | * 7 | * It is important to provide valid `target`, because Google Docs 8 | * uses special target for text events, not default `document`. 9 | * 10 | * Use this for help - https://keycode.info 11 | */ 12 | 13 | 14 | /** 15 | * Creates keyboard event. 16 | * 17 | * @param {'keypress' | 'keydown' | 'keyup'} name 18 | * Name of event. 19 | * @param {Document | HTMLElement} target 20 | * Target of event. 21 | * @param {string} key 22 | * Name of key. 23 | * @param {string | null} code 24 | * Code of `key`. Specify `null` for autodetect. 25 | * Autodetect works correctly only for letters. 26 | * @param {number | null} keyCode 27 | * "Numerical code identifying the unmodified value of the pressed key". 28 | * Specify `null` for autodetect. 29 | * @param {KeyboardEventInit} eventOptions 30 | * Custom options to add/overwrite event options. 31 | */ 32 | function createKeyboardEvent( 33 | name, 34 | target, 35 | key, 36 | code, 37 | keyCode, 38 | eventOptions 39 | ) { 40 | if (code == null) { 41 | code = 'Key' + key.toUpperCase(); 42 | } 43 | 44 | if (keyCode == null) { 45 | // `codePointAt`, not `charCodeAt`, because of 46 | // eslint-disable-next-line max-len 47 | // https://github.com/Amaimersion/google-docs-utils/issues/8#issuecomment-824117587 48 | keyCode = key.codePointAt(0); 49 | } 50 | 51 | return new KeyboardEvent( 52 | name, 53 | { 54 | repeat: false, 55 | isComposing: false, 56 | bubbles: true, 57 | cancelable: true, 58 | ctrlKey: false, 59 | shiftKey: false, 60 | altKey: false, 61 | metaKey: false, 62 | target: target, 63 | currentTarget: target, 64 | key: key, 65 | code: code, 66 | 67 | // it is important to also specify 68 | // these deprecated properties 69 | keyCode: keyCode, 70 | charCode: keyCode, 71 | which: keyCode, 72 | 73 | ...eventOptions 74 | } 75 | ); 76 | } 77 | 78 | 79 | /** 80 | * @param {Document | HTMLElement} target 81 | * @param {string} key 82 | * @param {string | null} code 83 | * @param {number | null} keyCode 84 | * @param {KeyboardEventInit} eventOptions 85 | */ 86 | export function keypress( 87 | target, 88 | key, 89 | code = null, 90 | keyCode = null, 91 | eventOptions = {} 92 | ) { 93 | const event = createKeyboardEvent( 94 | 'keypress', 95 | target, 96 | key, 97 | code, 98 | keyCode, 99 | eventOptions 100 | ); 101 | 102 | target.dispatchEvent(event); 103 | } 104 | 105 | 106 | /** 107 | * @param {Document | HTMLElement} target 108 | * @param {string} key 109 | * @param {string | null} code 110 | * @param {number | null} keyCode 111 | * @param {KeyboardEventInit} eventOptions 112 | */ 113 | export function keydown( 114 | target, 115 | key, 116 | code = null, 117 | keyCode = null, 118 | eventOptions = {} 119 | ) { 120 | const event = createKeyboardEvent( 121 | 'keydown', 122 | target, 123 | key, 124 | code, 125 | keyCode, 126 | eventOptions 127 | ); 128 | 129 | target.dispatchEvent(event); 130 | } 131 | -------------------------------------------------------------------------------- /src/common/query-selector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets HTML element using `querySelector`. 3 | * 4 | * @param {string[]} selectors 5 | * Array of possible selectors. 6 | * If selector results to some element, 7 | * then that element will be returned, 8 | * otherwise next selector will be used. 9 | * @param {document | HTMLElement} root 10 | * A root in which the element will be searched. 11 | * Defaults to `document`. 12 | * 13 | * @returns {HTMLElement | null} 14 | * HTML element if finded, `null` otherwise. 15 | * 16 | * @throws 17 | * Throws an error if `root == null`. 18 | */ 19 | export function querySelector( 20 | selectors, 21 | root = document 22 | ) { 23 | if (root == null) { 24 | throw new Error('Passed root element does not exists'); 25 | } 26 | 27 | let value = null; 28 | 29 | for (const selector of selectors) { 30 | value = root.querySelector(selector); 31 | 32 | if (value) { 33 | break; 34 | } 35 | } 36 | 37 | return value; 38 | } 39 | 40 | 41 | /** 42 | * Gets all HTML elements using `querySelectorAll`. 43 | * 44 | * @param {string[]} selectors 45 | * Array of possible selectors. 46 | * If selector results to some elements, 47 | * then these elements will be returned, 48 | * otherwise next selector will be used. 49 | * @param {document | HTMLElement} root 50 | * A root in which elements will be searched. 51 | * Defaults to `document`. 52 | * 53 | * @returns {HTMLElement[]} 54 | * HTML elements if finded, otherwise empty array. 55 | * 56 | * @throws 57 | * Throws an error if `root == null`. 58 | */ 59 | export function querySelectorAll( 60 | selectors, 61 | root = document 62 | ) { 63 | if (root == null) { 64 | throw new Error('Passed root element does not exists'); 65 | } 66 | 67 | let value = null; 68 | 69 | for (const selector of selectors) { 70 | value = root.querySelectorAll(selector); 71 | 72 | if (value.length > 0) { 73 | break; 74 | } 75 | } 76 | 77 | if (value) { 78 | return Array.from(value); 79 | } 80 | 81 | return []; 82 | } 83 | -------------------------------------------------------------------------------- /src/common/selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Selectors to find element in the page. 3 | * 4 | * Use array of strings, not just single string value. 5 | * It is means there can be multiple selectors for single 6 | * element, in descending order of priority. 7 | * For example, if selector № 1 returned some result, then 8 | * that result will be used, otherwise selector № 2 will 9 | * be used to try to get valid result, etc. 10 | * If there only one value, then use array with one element. 11 | */ 12 | 13 | 14 | export const docsEditorContainer = [ 15 | '#docs-editor-container' 16 | ]; 17 | 18 | export const docsEditor = [ 19 | '#docs-editor', 20 | ...docsEditorContainer 21 | ]; 22 | 23 | export const textEventTarget = [ 24 | 'iframe.docs-texteventtarget-iframe', 25 | '.docs-texteventtarget-iframe' 26 | ]; 27 | 28 | export const kixPage = [ 29 | '.kix-page', 30 | '.docs-page' 31 | ]; 32 | 33 | export const kixLine = [ 34 | '.kix-lineview', 35 | '.kix-paragraphrenderer' 36 | ]; 37 | 38 | export const kixLineText = [ 39 | '.kix-lineview-text-block' 40 | ]; 41 | 42 | export const kixWordNone = [ 43 | '.kix-wordhtmlgenerator-word-node' 44 | ]; 45 | 46 | export const kixSelectionOverlay = [ 47 | '.kix-selection-overlay' 48 | ]; 49 | 50 | export const kixCursor = [ 51 | '.kix-cursor' 52 | ]; 53 | 54 | export const kixActiveCursor = [ 55 | '.docs-text-ui-cursor-blink' 56 | ]; 57 | 58 | export const kixCursorCaret = [ 59 | '.kix-cursor-caret' 60 | ]; 61 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Joins text using separator. 3 | */ 4 | export function joinText(separator, ...args) { 5 | return args.join(separator); 6 | } 7 | 8 | 9 | /** 10 | * @param {HTMLElement} element 11 | */ 12 | export function isIframe(element) { 13 | return (element.nodeName.toLowerCase() === 'iframe'); 14 | } 15 | 16 | 17 | /** 18 | * NOTE: during execution temp element will be added 19 | * in the DOM. That element will be invisible to user, 20 | * and that element will be removed in the end of execution. 21 | * 22 | * @param {string} char 23 | * Single character is expected. 24 | * You can pass more than one character, 25 | * but result will be not so accurate, because, 26 | * for example, different characters may have different width. 27 | * @param {string} css 28 | * Using that CSS `char` was rendered. 29 | * It is important to provide exactly CSS 30 | * that was used for rendering, because 31 | * different CSS may lead to different rect. 32 | * 33 | * @returns {DOMRectReadOnly} 34 | * Rect of rendered character. 35 | */ 36 | export function getCharRect(char, css) { 37 | const element = document.createElement('span'); 38 | 39 | element.textContent = char; 40 | element.style.cssText = css; 41 | 42 | // sequences of white spaces should be preserved 43 | element.style.whiteSpace = 'pre'; 44 | 45 | // don't display this element this element 46 | element.style.position = 'absolute'; 47 | element.style.top = '-100px'; 48 | 49 | // need to render this element in order to get valid rect 50 | document.body.appendChild(element); 51 | 52 | const rect = element.getBoundingClientRect(); 53 | 54 | element.remove(); 55 | 56 | return rect; 57 | } 58 | 59 | 60 | /** 61 | * @param {DOMRectReadOnly} a 62 | * @param {DOMRectReadOnly} b 63 | * 64 | * @returns {boolean} 65 | * Two rects overlaps each other. 66 | * 67 | * @see https://stackoverflow.com/a/306332/8445442 68 | */ 69 | export function isRectsOverlap(a, b) { 70 | return ( 71 | (a.left <= b.right) && 72 | (a.right >= b.left) && 73 | (a.top <= b.bottom) && 74 | (a.bottom >= b.top) 75 | ); 76 | } 77 | 78 | 79 | /** 80 | * Similar to RegExp `\w`, but also supports non-ASCII characters 81 | * 82 | * WARNING: 83 | * it will not work for Chinese, Japanese, Arabic, Hebrew and most 84 | * other scripts which doesn't have upper and lower latters. 85 | * 86 | * @param {string} char 87 | * Char to check. 88 | */ 89 | export function charIsWordChar(char) { 90 | // ASCII, numbers, underscores and other symbols 91 | if (char.match(/[\w]/)) { 92 | return true; 93 | } 94 | 95 | // https://stackoverflow.com/a/32567789/8445442 96 | if (char.toLowerCase() !== char.toUpperCase()) { 97 | return true; 98 | } 99 | 100 | return false; 101 | } 102 | 103 | 104 | /** 105 | * Runs a method when page is fully loaded. 106 | * 107 | * @param {Function} method 108 | * Method to run. 109 | */ 110 | export function runOnPageLoaded(method) { 111 | // inherit `this` context. 112 | const mthd = () => { 113 | method(); 114 | }; 115 | 116 | if (document.readyState === 'loading') { 117 | document.addEventListener('DOMContentLoaded', mthd); 118 | } else { 119 | mthd(); 120 | } 121 | } 122 | 123 | 124 | /** 125 | * Converts selectors (`selectors.js`) to list of class names. 126 | * 127 | * @param {string[]} selectors 128 | * Selectors variable from `selectors.js` file. 129 | * 130 | * @returns 131 | * Class names one by one, without dot. 132 | * See example for more. 133 | * 134 | * @example 135 | * ([ 136 | * '.test', '.test2.iframe', '#nide.hide', 137 | * 'div.div1.div2', '#tag' 138 | * ]) => [ 139 | * 'test', 'test2', 'iframe', 'hide', 140 | * 'div1', 'div2' 141 | * ] 142 | */ 143 | export function selectorsToClassList(selectors) { 144 | const result = []; 145 | 146 | for (let selector of selectors) { 147 | if (!selector.startsWith('.')) { 148 | selector = selector.slice( 149 | selector.indexOf('.') 150 | ); 151 | } 152 | 153 | selector = selector.slice(1); 154 | const classNames = selector.split('.'); 155 | 156 | for (const className of classNames) { 157 | if (className) { 158 | result.push(className); 159 | } 160 | } 161 | } 162 | 163 | return result; 164 | } 165 | -------------------------------------------------------------------------------- /src/focus-document.js: -------------------------------------------------------------------------------- 1 | import isDocumentActive from './is-document-active'; 2 | import isTextSelected from './is-text-selected'; 3 | import { 4 | Character as PressOnCharacter, 5 | Undo as PressOnUndo, 6 | Backspace as PressOnBackspace 7 | } from './press-on'; 8 | 9 | 10 | /** 11 | * Focuses on current document. 12 | * 13 | * "Focus" means that document is active and available for editing: 14 | * cursor is blinking or selection active. 15 | * 16 | * @returns {boolean} 17 | * `true` if there was any actions to perform a focus, 18 | * `false` if document already was active and nothing was performed. 19 | */ 20 | export default function focusDocument() { 21 | if (isDocumentActive()) { 22 | return false; 23 | } 24 | 25 | // character that is acceptable by Google Docs should be used. 26 | // For example, `\u0000` is not acceptable and will be not typed. 27 | // Use something from this plane: 28 | // https://www.compart.com/en/unicode/plane/U+0000 29 | const randomCharToCreateFocus = '\u003F'; 30 | 31 | const textSelected = isTextSelected(); 32 | 33 | PressOnCharacter(randomCharToCreateFocus); 34 | 35 | // if selection existed, then at the moment we removed it. 36 | // lets restore it, otherwise we will delete typed character 37 | if (textSelected) { 38 | PressOnUndo(); 39 | } else { 40 | PressOnBackspace(); 41 | } 42 | 43 | return true; 44 | } 45 | -------------------------------------------------------------------------------- /src/get-active-cursor-element.js: -------------------------------------------------------------------------------- 1 | import {querySelector} from './common/query-selector'; 2 | import {kixActiveCursor} from './common/selectors'; 3 | import getEditorElement from './get-editor-element'; 4 | 5 | 6 | /** 7 | * @returns {HTMLElement} 8 | * User active blinked cursor. 9 | */ 10 | export default function getActiveCursorElement() { 11 | const editor = getEditorElement(); 12 | 13 | return querySelector(kixActiveCursor, editor); 14 | } 15 | -------------------------------------------------------------------------------- /src/get-caret-element.js: -------------------------------------------------------------------------------- 1 | import {querySelector} from './common/query-selector'; 2 | import {kixCursorCaret} from './common/selectors'; 3 | import getCursorElement from './get-cursor-element'; 4 | 5 | 6 | /** 7 | * @returns {HTMLElement | null} 8 | * Caret of user active cursor. 9 | */ 10 | export default function getCaretElement() { 11 | const activeCursor = getCursorElement(); 12 | 13 | if (!activeCursor) { 14 | return null; 15 | } 16 | 17 | return querySelector(kixCursorCaret, activeCursor); 18 | } 19 | -------------------------------------------------------------------------------- /src/get-caret-word.js: -------------------------------------------------------------------------------- 1 | import {charIsWordChar} from './common/utils'; 2 | import getCaret from './get-caret'; 3 | import clearTextContent from './clear-text-content'; 4 | 5 | 6 | /** 7 | * @returns 8 | * A word on which caret is currently located. 9 | */ 10 | export default function getCaretWord() { 11 | const caret = getCaret(); 12 | 13 | if (!caret) { 14 | return null; 15 | } 16 | 17 | const caretText = clearTextContent(caret.wordElement.textContent); 18 | const result = { 19 | word: '', 20 | text: caretText, 21 | indexStart: caret.positionIndexRelativeToWord, 22 | indexEnd: caret.positionIndexRelativeToWord 23 | }; 24 | 25 | // not strict `>=`, because we may shift 26 | // by one to the left in further 27 | if (caret.positionIndexRelativeToWord > caretText.length) { 28 | return result; 29 | } 30 | 31 | const indexStart = getBoundaryIndex( 32 | caret.positionIndexRelativeToWord, 33 | caretText, 34 | true 35 | ); 36 | const indexEnd = getBoundaryIndex( 37 | caret.positionIndexRelativeToWord, 38 | caretText, 39 | false 40 | ); 41 | 42 | result.indexStart = indexStart; 43 | result.indexEnd = indexEnd; 44 | result.word = caretText.substring(indexStart, indexEnd); 45 | 46 | return result; 47 | } 48 | 49 | 50 | /** 51 | * @param {number} startIndex 52 | * From where to start search a word boundary. 53 | * @param {string} text 54 | * Full text. 55 | * @param {bool} toLeft 56 | * `true` for left direction, 57 | * `false` for right direction. 58 | * 59 | * @returns {number} 60 | * Index of word boundary that can be used for `substring()`. 61 | * 62 | * @example 63 | * ``` 64 | * const text = 'one two three'; 65 | * const start = getBoundaryIndex(5, text, true) // => 4; 66 | * const end = getBoundaryIndex(5, text, false) // => 7; 67 | * 68 | * text.substring(start, end); // => 'two' 69 | * ``` 70 | * 71 | * @example 72 | * ``` 73 | * const text = 'one two three'; 74 | * const start = getBoundaryIndex(3, text, true) // => 0; 75 | * const end = getBoundaryIndex(3, text, false) // => 3; 76 | * 77 | * text.substring(start, end); // => 'one' 78 | * ``` 79 | * 80 | * @example 81 | * ``` 82 | * const text = 'one two three'; // notice extra space 83 | * const start = getBoundaryIndex(4, text, true) // => 4; 84 | * const end = getBoundaryIndex(4, text, false) // => 4; 85 | * 86 | * text.substring(start, end); // => '' 87 | * ``` 88 | * 89 | * @example 90 | * ``` 91 | * const text = ' one two three'; // notice extra spaces 92 | * const start = getBoundaryIndex(1, text, true) // => 1; 93 | * const end = getBoundaryIndex(1, text, false) // => 1; 94 | * 95 | * text.substring(start, end); // => 'one' 96 | * ``` 97 | */ 98 | function getBoundaryIndex(startIndex, text, toLeft) { 99 | let isEnd = undefined; 100 | let move = undefined; 101 | let undoMove = undefined; 102 | 103 | if (toLeft) { 104 | isEnd = (index) => (index <= 0); 105 | move = (index) => (index - 1); 106 | undoMove = (index) => (index + 1); 107 | } else { 108 | isEnd = (index) => (index >= text.length); 109 | move = (index) => (index + 1); 110 | undoMove = (index) => (index - 1); 111 | } 112 | 113 | let boundaryIndex = startIndex; 114 | let character = text[boundaryIndex]; 115 | 116 | // in case if we at the end of word, 117 | // let's shift to the left by one in order 118 | // next `while` algorithm handle that case correctly 119 | if ( 120 | toLeft && 121 | charIsOutOfWord(character) && 122 | !isEnd(boundaryIndex) 123 | ) { 124 | boundaryIndex = move(boundaryIndex); 125 | character = text[boundaryIndex]; 126 | 127 | // there is no word boundary after shift by one, 128 | // we should initial start index without move 129 | if (charIsOutOfWord(character)) { 130 | return startIndex; 131 | } 132 | } 133 | 134 | while ( 135 | !charIsOutOfWord(character) && 136 | !isEnd(boundaryIndex) 137 | ) { 138 | boundaryIndex = move(boundaryIndex); 139 | character = text[boundaryIndex]; 140 | } 141 | 142 | // if previous `while` ended because of `charIsOutOfWord`, 143 | // then now we have boundary index for invalid character. 144 | // It is expected result for `toLeft = false` because in that 145 | // case we want exclude such character from `substring()`, 146 | // but in case of `toLeft = true` we don't want include invalid 147 | // word boundary character in `substring()`. 148 | if ( 149 | toLeft && 150 | !isEnd(boundaryIndex) 151 | ) { 152 | boundaryIndex = undoMove(boundaryIndex); 153 | } 154 | 155 | return boundaryIndex; 156 | } 157 | 158 | 159 | /** 160 | * @param {string} character 161 | * 162 | * @returns 163 | * Character is outside of word boundary. 164 | */ 165 | function charIsOutOfWord(character) { 166 | if (character == null) { 167 | return true; 168 | } 169 | 170 | return !charIsWordChar(character); 171 | } 172 | -------------------------------------------------------------------------------- /src/get-caret.js: -------------------------------------------------------------------------------- 1 | import {isRectsOverlap, getCharRect} from './common/utils'; 2 | import getCaretElement from './get-caret-element'; 3 | import getWordElements from './get-word-elements'; 4 | 5 | 6 | /** 7 | * @returns 8 | * Information about caret. 9 | * `null` if unable to get information. 10 | */ 11 | export default function getCaret() { 12 | const caretElement = getCaretElement(); 13 | 14 | if (!caretElement) { 15 | return null; 16 | } 17 | 18 | const wordElements = getWordElements(); 19 | 20 | if (!wordElements.length) { 21 | return null; 22 | } 23 | 24 | const caretRect = caretElement.getBoundingClientRect(); 25 | const result = { 26 | element: null, 27 | wordElement: null, 28 | lineIndex: null, 29 | positionIndexRelativeToWord: null 30 | }; 31 | let resultFound = false; 32 | 33 | for (let lineIndex = 0; lineIndex !== wordElements.length; lineIndex++) { 34 | const line = wordElements[lineIndex]; 35 | 36 | for (let wordIndex = 0; wordIndex !== line.length; wordIndex++) { 37 | const wordElement = line[wordIndex]; 38 | const wordRect = wordElement.getBoundingClientRect(); 39 | const isOverlap = isRectsOverlap(caretRect, wordRect); 40 | 41 | if (!isOverlap) { 42 | continue; 43 | } 44 | 45 | result.element = caretElement; 46 | result.wordElement = wordElement; 47 | result.lineIndex = lineIndex; 48 | result.positionIndexRelativeToWord = calculatePositionIndex( 49 | wordRect, 50 | caretRect, 51 | wordElement.textContent, 52 | wordElement.style.cssText 53 | ); 54 | resultFound = true; 55 | 56 | break; 57 | } 58 | 59 | if (resultFound) { 60 | break; 61 | } 62 | } 63 | 64 | return result; 65 | } 66 | 67 | 68 | /** 69 | * @param {DOMRectReadOnly} wordRect 70 | * @param {DOMRectReadOnly} caretRect 71 | * @param {string} text 72 | * "Dirty" `textContent` is expected. 73 | * In case of "Dirty" empty spaces will be 74 | * handled correctly. 75 | * @param {string} textCSS 76 | * 77 | * @returns 78 | * On what position caret is placed on a line. 79 | * For example, `1` means caret is placed before 80 | * second character of line text. 81 | */ 82 | function calculatePositionIndex(wordRect, caretRect, text, textCSS) { 83 | let virtualCaretLeft = wordRect.left - caretRect.width; 84 | let localIndex = 0; 85 | 86 | for (const char of text) { 87 | const charRect = getCharRect(char, textCSS); 88 | 89 | // we should ignore special invisible 90 | // characters like ZWNJ or NBSP 91 | if (charRect.width === 0) { 92 | continue; 93 | } 94 | 95 | virtualCaretLeft += charRect.width; 96 | 97 | if (virtualCaretLeft >= caretRect.left) { 98 | break; 99 | } 100 | 101 | localIndex += 1; 102 | } 103 | 104 | return localIndex; 105 | } 106 | -------------------------------------------------------------------------------- /src/get-cursor-element.js: -------------------------------------------------------------------------------- 1 | import {querySelector} from './common/query-selector'; 2 | import {kixCursor} from './common/selectors'; 3 | import getEditorElement from './get-editor-element'; 4 | 5 | 6 | /** 7 | * @returns {HTMLElement} 8 | * User cursor. 9 | */ 10 | export default function getCursorElement() { 11 | const editor = getEditorElement(); 12 | 13 | return querySelector(kixCursor, editor); 14 | } 15 | -------------------------------------------------------------------------------- /src/get-editor-element.js: -------------------------------------------------------------------------------- 1 | import {docsEditor} from './common/selectors'; 2 | import {querySelector} from './common/query-selector'; 3 | 4 | 5 | /** 6 | * @returns 7 | * Current active editor element. 8 | * NOTE: it contains only editor element itself, 9 | * not bar and other elements. 10 | */ 11 | export default function getEditorElement() { 12 | return querySelector(docsEditor); 13 | } 14 | -------------------------------------------------------------------------------- /src/get-line-text.js: -------------------------------------------------------------------------------- 1 | import getLinesText from './get-lines-text'; 2 | 3 | 4 | /** 5 | * @param {number} lineIndex 6 | * @param {number} startIndex 7 | * @param {number} endIndex 8 | * 9 | * @returns 10 | * Text of specific line. 11 | */ 12 | export default function getLineText( 13 | lineIndex, 14 | startIndex = undefined, 15 | endIndex = undefined 16 | ) { 17 | const linesText = getLinesText(); 18 | 19 | if (lineIndex >= linesText.length) { 20 | return null; 21 | } 22 | 23 | const text = linesText[lineIndex]; 24 | 25 | if (startIndex == null) { 26 | startIndex = 0; 27 | } 28 | 29 | if (endIndex == null) { 30 | endIndex = text.length; 31 | } 32 | 33 | return text.substring(startIndex, endIndex); 34 | } 35 | -------------------------------------------------------------------------------- /src/get-lines-elements.js: -------------------------------------------------------------------------------- 1 | import {kixLine} from './common/selectors'; 2 | import {querySelectorAll} from './common/query-selector'; 3 | import getPagesElements from './get-pages-elements'; 4 | 5 | 6 | /** 7 | * @returns {HTMLElement[]} 8 | * All rendered pages of editor. 9 | */ 10 | export default function getLinesElements() { 11 | const pages = getPagesElements(); 12 | let result = []; 13 | 14 | for (const page of pages) { 15 | const lines = querySelectorAll(kixLine, page); 16 | result = [ 17 | ...result, 18 | ...lines 19 | ]; 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/get-lines-text-elements.js: -------------------------------------------------------------------------------- 1 | import {kixLineText} from './common/selectors'; 2 | import {querySelector} from './common/query-selector'; 3 | import getLinesElements from './get-lines-elements'; 4 | 5 | 6 | /** 7 | * @returns {HTMLElement[]} 8 | * Text elements of each line. 9 | * Every text element contains all word elements 10 | * (there can be multiple word elements for one text element). 11 | */ 12 | export default function getLinesTextElements() { 13 | const lines = getLinesElements(); 14 | const result = []; 15 | 16 | for (const line of lines) { 17 | const textElement = querySelector(kixLineText, line); 18 | 19 | result.push(textElement); 20 | } 21 | 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/get-lines-text.js: -------------------------------------------------------------------------------- 1 | import getLinesTextElements from './get-lines-text-elements'; 2 | import clearTextContent from './clear-text-content'; 3 | 4 | 5 | /** 6 | * @returns 7 | * Text of every editor line. 8 | * If line is empty, then zero length string 9 | * will be returned for that line. 10 | */ 11 | export default function getLinesText() { 12 | const lines = getLinesTextElements(); 13 | const result = []; 14 | 15 | for (const line of lines) { 16 | // difference between `textContent` and `innerText` is matters! 17 | let value = line.textContent; 18 | 19 | value = clearTextContent(value); 20 | value = clearLineText(value); 21 | 22 | result.push(value); 23 | } 24 | 25 | return result; 26 | } 27 | 28 | 29 | /** 30 | * @param {string} value 31 | */ 32 | function clearLineText(value) { 33 | return value.trim(); 34 | } 35 | -------------------------------------------------------------------------------- /src/get-pages-elements.js: -------------------------------------------------------------------------------- 1 | import {kixPage} from './common/selectors'; 2 | import {querySelectorAll} from './common/query-selector'; 3 | import getEditorElement from './get-editor-element'; 4 | 5 | 6 | /** 7 | * @returns 8 | * All rendered pages of editor. 9 | */ 10 | export default function getPagesElements() { 11 | const editor = getEditorElement(); 12 | 13 | return querySelectorAll(kixPage, editor); 14 | } 15 | -------------------------------------------------------------------------------- /src/get-selection-overlay-elements.js: -------------------------------------------------------------------------------- 1 | import {kixSelectionOverlay} from './common/selectors'; 2 | import {querySelector} from './common/query-selector'; 3 | import getLinesElements from './get-lines-elements'; 4 | 5 | 6 | /** 7 | * Google Docs creates separate element to display 8 | * selection. It is no actual selection of text, it is 9 | * just an element with some style that emulates selection. 10 | * 11 | * Because of this, for example, you cannot just remove 12 | * selection overlay element from DOM in order to remove selection, 13 | * because Google Docs will restore selection at next user selection. 14 | * 15 | * @returns {HTMLElement[]} 16 | * Selection overlay element for each line. 17 | * If there are no selection for that line, 18 | * then `null` will be used. 19 | */ 20 | export default function getSelectionOverlayElements() { 21 | const lines = getLinesElements(); 22 | const result = []; 23 | 24 | for (const line of lines) { 25 | const element = querySelector(kixSelectionOverlay, line); 26 | result.push(element); 27 | } 28 | 29 | return result; 30 | } 31 | -------------------------------------------------------------------------------- /src/get-selection.js: -------------------------------------------------------------------------------- 1 | import {getCharRect} from './common/utils'; 2 | import getSelectionElements from './get-selection-overlay-elements'; 3 | import getWordElements from './get-word-elements'; 4 | import clearTextContent from './clear-text-content'; 5 | 6 | 7 | /** 8 | * @returns {Array>} 9 | * Selection data for every rendered line. 10 | * `[]` - represents line, `[][]` - represents all 11 | * selected word nodes. 12 | * `[]` - element will be `null` if that line doesn't 13 | * contains selection at all, otherwise it will be array. 14 | * `[][]` - it is all selected word nodes (see `getWordElements()` 15 | * documentation for more). If word node not selected (i.e., selection 16 | * don't overlaps that node), then value will be `null`, otherwise 17 | * it will be an object that describes selection of that word node. 18 | * 19 | * @throws 20 | * Throws an error if unable to get information 21 | * about current selection for at least one line. 22 | */ 23 | export default function getSelection() { 24 | const selectionElements = getSelectionElements(); 25 | const wordElements = getWordElements(); 26 | 27 | if (selectionElements.length !== wordElements.length) { 28 | throw new Error( 29 | 'Unable to map selection elements and word elements' 30 | ); 31 | } 32 | 33 | const count = Math.min( 34 | selectionElements.length, 35 | wordElements.length 36 | ); 37 | const result = []; 38 | const emptyValue = null; 39 | 40 | for (let i = 0; i !== count; i++) { 41 | const selectionElement = selectionElements[i]; 42 | 43 | if (!selectionElement) { 44 | result.push(emptyValue); 45 | 46 | continue; 47 | } 48 | 49 | const line = wordElements[i]; 50 | const lineSelection = []; 51 | 52 | for (const wordElement of line) { 53 | if (!wordElement) { 54 | lineSelection.push(emptyValue); 55 | 56 | continue; 57 | } 58 | 59 | const originalText = clearTextContent(wordElement.textContent); 60 | const textCSS = wordElement.style.cssText; 61 | const wordRect = wordElement.getBoundingClientRect(); 62 | const selectionRect = selectionElement.getBoundingClientRect(); 63 | const selectionIndexes = calculateSelectionIndexes( 64 | originalText, 65 | textCSS, 66 | wordRect, 67 | selectionRect 68 | ); 69 | const notSelected = (!selectionIndexes); 70 | 71 | if (notSelected) { 72 | lineSelection.push(emptyValue); 73 | 74 | continue; 75 | } 76 | 77 | const selectedText = originalText.substring( 78 | selectionIndexes.start, 79 | selectionIndexes.end 80 | ); 81 | 82 | lineSelection.push({ 83 | text: originalText, 84 | selectedText: selectedText, 85 | selectionStart: selectionIndexes.start, 86 | selectionEnd: selectionIndexes.end, 87 | textRect: wordRect, 88 | selectionRect: selectionRect, 89 | textElement: wordElement, 90 | selectionElement: selectionElement 91 | }); 92 | } 93 | 94 | result.push(lineSelection); 95 | } 96 | 97 | return result; 98 | } 99 | 100 | 101 | /** 102 | * Calculates text selection indexes based on 103 | * DOM rect of text element and selection element. 104 | * 105 | * @param {string} text 106 | * Original text. 107 | * @param {string} textCSS 108 | * CSS of rendered original text. 109 | * @param {DOMRectReadOnly} textRect 110 | * DOM rect of text element. 111 | * @param {DOMRectReadOnly} selectionRect 112 | * DOM rect of selection element. 113 | * 114 | * @returns 115 | * Indexes of current text selection. 116 | * They can be used, for example, for `substring()`. 117 | * `null` will be returned if nothing is selected. 118 | */ 119 | function calculateSelectionIndexes( 120 | text, 121 | textCSS, 122 | textRect, 123 | selectionRect 124 | ) { 125 | let virtualCaretLeft = textRect.left; 126 | let selected = false; 127 | let selectionStart = 0; 128 | let selectionEnd = text.length; 129 | 130 | for (let i = 0; i !== text.length; i++) { 131 | const isOverlap = ( 132 | (selectionRect.left <= virtualCaretLeft) && 133 | (virtualCaretLeft < selectionRect.right) 134 | ); 135 | 136 | if (isOverlap) { 137 | if (!selected) { 138 | selectionStart = i; 139 | selected = true; 140 | } 141 | } else { 142 | if (selected) { 143 | selectionEnd = i; 144 | break; 145 | } 146 | } 147 | 148 | const char = text[i]; 149 | const charRect = getCharRect(char, textCSS); 150 | virtualCaretLeft += charRect.width; 151 | } 152 | 153 | const selectionIndexes = { 154 | start: selectionStart, 155 | end: selectionEnd, 156 | }; 157 | 158 | return (selected ? selectionIndexes : null); 159 | } 160 | -------------------------------------------------------------------------------- /src/get-text-event-target.js: -------------------------------------------------------------------------------- 1 | import {textEventTarget} from './common/selectors'; 2 | import {querySelector} from './common/query-selector'; 3 | import {isIframe} from './common/utils'; 4 | 5 | 6 | /** 7 | * - Google Docs uses special target to handle 8 | * text events. So, for example, you cannot send 9 | * text event just to current document. You 10 | * should use special target for that. 11 | * 12 | * @returns {HTMLElement | Document} 13 | * A target that can be used to send text events 14 | * and listens for text events (in particular, keyboard events). 15 | */ 16 | export default function getTextEventTarget() { 17 | /** 18 | * @type {HTMLElement & HTMLIFrameElement} 19 | */ 20 | const element = querySelector(textEventTarget); 21 | 22 | if (isIframe(element)) { 23 | return element.contentDocument; 24 | } 25 | 26 | return element; 27 | } 28 | -------------------------------------------------------------------------------- /src/get-word-elements.js: -------------------------------------------------------------------------------- 1 | import {kixWordNone} from './common/selectors'; 2 | import {querySelectorAll} from './common/query-selector'; 3 | import getLinesElements from './get-lines-elements'; 4 | 5 | 6 | /** 7 | * @returns {Array} 8 | * Each element is a line, each of elements 9 | * of that line is a word node of that line. 10 | * These word nodes contains actual text of line. 11 | * 12 | * NOTE: 13 | * if text of line contains various formatting (font, bold, etc.), 14 | * then it will be splitted into several word nodes. 15 | * For example, "some [Arial font] text [Roboto font]" will be 16 | * splitted into two nodes, "some text [Arial font]" will be 17 | * represented as one node and "another [Arial font, normal] 18 | * text [Arial font, bold]" will be splitted into two nodes. 19 | */ 20 | export default function getWordElements() { 21 | const lines = getLinesElements(); 22 | const result = []; 23 | 24 | for (const line of lines) { 25 | const nodes = querySelectorAll(kixWordNone, line); 26 | result.push(nodes); 27 | } 28 | 29 | return result; 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file exposes methods that is intended for public usage. 3 | * 4 | * - order and structure of imports doesn't matter 5 | */ 6 | 7 | 8 | import getEditorElement from './get-editor-element'; 9 | import getTextEventTarget from './get-text-event-target'; 10 | import getPagesElements from './get-pages-elements'; 11 | import getLinesElements from './get-lines-elements'; 12 | import getLinesTextElements from './get-lines-text-elements'; 13 | import getLinesText from './get-lines-text'; 14 | import getLineText from './get-line-text'; 15 | import clearTextContent from './clear-text-content'; 16 | import getWordElements from './get-word-elements'; 17 | import getSelectionOverlayElements from './get-selection-overlay-elements'; 18 | import getSelection from './get-selection'; 19 | import getCursorElement from './get-cursor-element'; 20 | import getActiveCursorElement from './get-active-cursor-element'; 21 | import getCaretElement from './get-caret-element'; 22 | import getCaret from './get-caret'; 23 | import getCaretWord from './get-caret-word'; 24 | import * as pressOn from './press-on'; 25 | import typeText from './type-text'; 26 | import isTextSelected from './is-text-selected'; 27 | import isDocumentActive from './is-document-active'; 28 | import focusDocument from './focus-document'; 29 | import * as moveCursorTo from './move-cursor-to'; 30 | import * as remove from './remove'; 31 | import * as select from './select'; 32 | import { 33 | addEventListener 34 | } from './mutation-observer'; 35 | 36 | 37 | export { 38 | getEditorElement, 39 | getTextEventTarget, 40 | getPagesElements, 41 | getLinesElements, 42 | getLinesTextElements, 43 | getLinesText, 44 | getLineText, 45 | clearTextContent, 46 | getWordElements, 47 | getSelectionOverlayElements, 48 | getSelection, 49 | getCursorElement, 50 | getActiveCursorElement, 51 | getCaretElement, 52 | getCaret, 53 | getCaretWord, 54 | pressOn, 55 | typeText, 56 | isTextSelected, 57 | isDocumentActive, 58 | focusDocument, 59 | remove, 60 | moveCursorTo, 61 | select, 62 | addEventListener 63 | }; 64 | -------------------------------------------------------------------------------- /src/is-document-active.js: -------------------------------------------------------------------------------- 1 | import getActiveCursorElement from './get-active-cursor-element'; 2 | 3 | 4 | /** 5 | * @returns {boolean} 6 | * Document is focused and active. 7 | * It is means that cursor is blinked. 8 | */ 9 | export default function isDocumentActive() { 10 | const activeCursor = getActiveCursorElement(); 11 | const documentIsActive = !!activeCursor; 12 | 13 | return documentIsActive; 14 | } 15 | -------------------------------------------------------------------------------- /src/is-text-selected.js: -------------------------------------------------------------------------------- 1 | import getSelectionElements from './get-selection-overlay-elements'; 2 | 3 | 4 | /** 5 | * @returns {boolean} 6 | * Text selection is exists (at least one line). 7 | */ 8 | export default function isTextSelected() { 9 | const selectionElements = getSelectionElements(); 10 | const isSelected = selectionElements.some((i) => !!i); 11 | 12 | return isSelected; 13 | } 14 | -------------------------------------------------------------------------------- /src/move-cursor-to.js: -------------------------------------------------------------------------------- 1 | import * as pressOn from './press-on'; 2 | import focusDocument from './focus-document'; 3 | 4 | 5 | /** 6 | * Moves cursor to character that is placed to the left 7 | * of current cursor position. If that character placed 8 | * on previous line, then previous line will be used 9 | */ 10 | export function PrevCharacter() { 11 | pressOn.ArrowLeft(); 12 | } 13 | 14 | 15 | /** 16 | * Moves cursor to character that is placed to the right 17 | * of current cursor position. If that character placed 18 | * on next line, then next line will be used 19 | */ 20 | export function NextCharacter() { 21 | pressOn.ArrowRight(); 22 | } 23 | 24 | 25 | /** 26 | * Moves cursor to the previous line and tries to keep 27 | * cursor position. If there is no previous line, then moves 28 | * cursor to the start of current paragraph 29 | */ 30 | export function PrevLine() { 31 | pressOn.ArrowUp(); 32 | } 33 | 34 | 35 | /** 36 | * Moves cursor to the next line and tries to keep 37 | * cursor position. If there is no next line, then moves 38 | * cursor to the end of current paragraph 39 | */ 40 | export function NextLine() { 41 | pressOn.ArrowDown(); 42 | } 43 | 44 | 45 | /** 46 | * Moves cursor to: 47 | * - if it is start of current line, then to 48 | * the end of previous word on previous line 49 | * - else if it is start of current word, then to 50 | * the start of previous word 51 | * - else moves to the start of current word 52 | */ 53 | export function PrevWord() { 54 | pressOn.ArrowLeft({ 55 | ctrlKey: true 56 | }); 57 | } 58 | 59 | 60 | /** 61 | * Moves cursor to: 62 | * - if it is end of current line, then to 63 | * the start of next word on next line 64 | * - else if it is end of current word, then to 65 | * the end of next word 66 | * - else moves to the end of current word 67 | */ 68 | export function NextWord() { 69 | pressOn.ArrowRight({ 70 | ctrlKey: true 71 | }); 72 | } 73 | 74 | 75 | /** 76 | * Moves cursor to: 77 | * - if it is start of current paragraph, then to 78 | * the start of previous paragraph 79 | * - else moves to the start of current paragraph 80 | */ 81 | export function PrevParagraph() { 82 | pressOn.ArrowUp({ 83 | ctrlKey: true 84 | }); 85 | } 86 | 87 | 88 | /** 89 | * Moves cursor to: 90 | * - if it is end of current paragraph, then to 91 | * the end of next paragraph 92 | * - else moves to the end of current paragraph 93 | */ 94 | export function NextParagraph() { 95 | pressOn.ArrowDown({ 96 | ctrlKey: true 97 | }); 98 | } 99 | 100 | 101 | /** 102 | * Moves cursor to the start of current line. 103 | */ 104 | export function LineStart() { 105 | // focus is needed in order to behave properly 106 | focusDocument(); 107 | 108 | pressOn.Home(); 109 | } 110 | 111 | 112 | /** 113 | * Moves cursor to the end of current line. 114 | */ 115 | export function LineEnd() { 116 | // focus is needed in order to behave properly 117 | focusDocument(); 118 | 119 | pressOn.End(); 120 | } 121 | 122 | 123 | /** 124 | * Moves cursor to the start of document. 125 | */ 126 | export function DocumentStart() { 127 | pressOn.Home({ 128 | ctrlKey: true 129 | }); 130 | } 131 | 132 | 133 | /** 134 | * Moves cursor to the end of document. 135 | */ 136 | export function DocumentEnd() { 137 | pressOn.End({ 138 | ctrlKey: true 139 | }); 140 | } 141 | -------------------------------------------------------------------------------- /src/mutation-observer.js: -------------------------------------------------------------------------------- 1 | import { 2 | runOnPageLoaded, 3 | selectorsToClassList 4 | } from './common/utils'; 5 | import {querySelector} from './common/query-selector'; 6 | import * as selectors from './common/selectors'; 7 | 8 | 9 | /** 10 | * Type of Google Docs event. 11 | */ 12 | const EVENT_TYPE = { 13 | selectionChange: 'selectionchange' 14 | }; 15 | 16 | /** 17 | * Google Docs event listeners. 18 | * 19 | * Structure: 20 | * - key: event type 21 | * - value: all registered listeners for that event 22 | * 23 | * @type {{[key: string]: Function[]}} 24 | */ 25 | const EVENT_LISTENERS = {}; 26 | 27 | 28 | //#region Precalculated values 29 | 30 | const KIX_SELECTION_OVERLAY_CLASS_LIST = selectorsToClassList( 31 | selectors.kixSelectionOverlay 32 | ); 33 | 34 | //#endregion 35 | 36 | 37 | /** 38 | * Runs on script inject. 39 | */ 40 | function main() { 41 | runOnPageLoaded(bindObserver); 42 | } 43 | 44 | 45 | /** 46 | * Creates mutation observer and starts observing Google Docs container. 47 | * The container element should be created at that stage. 48 | */ 49 | function bindObserver() { 50 | const docsEditorContainer = querySelector(selectors.docsEditorContainer); 51 | 52 | if (docsEditorContainer == null) { 53 | throw new Error('Unable to observe missing docsEditorContainer'); 54 | } 55 | 56 | const observer = new MutationObserver(mutationCallback); 57 | 58 | observer.observe( 59 | docsEditorContainer, 60 | { 61 | subtree: true, 62 | childList: true, 63 | attributes: false, 64 | characterData: false 65 | } 66 | ); 67 | } 68 | 69 | 70 | /** 71 | * Callback which will be called on every Google Docs mutation. 72 | */ 73 | function mutationCallback(mutationList) { 74 | let selectionChangeEvent = false; 75 | 76 | // TODO: refactoring of that entire loop if there will be more events 77 | for (const mutation of mutationList) { 78 | for (const addedNode of mutation.addedNodes) { 79 | const addedNodeClassList = Array.from( 80 | addedNode.classList || [] 81 | ); 82 | 83 | selectionChangeEvent = ( 84 | selectionChangeEvent || 85 | KIX_SELECTION_OVERLAY_CLASS_LIST.some( 86 | (value) => addedNodeClassList.includes(value) 87 | ) 88 | ); 89 | } 90 | 91 | for (const removedNode of mutation.removedNodes) { 92 | const removedNodeClassList = Array.from( 93 | removedNode.classList || [] 94 | ); 95 | 96 | selectionChangeEvent = ( 97 | selectionChangeEvent || 98 | KIX_SELECTION_OVERLAY_CLASS_LIST.some( 99 | (value) => removedNodeClassList.includes(value) 100 | ) 101 | ); 102 | } 103 | } 104 | 105 | if (selectionChangeEvent) { 106 | callEventListener(EVENT_TYPE.selectionChange); 107 | } 108 | } 109 | 110 | 111 | /** 112 | * Adds listener for specific event. 113 | * 114 | * There can be many listeners for single event. 115 | * Order of calling is same as order of adding. 116 | * 117 | * @param {string} type 118 | * Type of event. Use `EVENT_TYPE`. 119 | * @param {(event: object) => any} listener 120 | * Callback that will be called. 121 | * Information about event will be passed as argument. 122 | */ 123 | export function addEventListener(type, listener) { 124 | if (!EVENT_LISTENERS[type]) { 125 | EVENT_LISTENERS[type] = []; 126 | } 127 | 128 | EVENT_LISTENERS[type].push(listener); 129 | } 130 | 131 | 132 | /** 133 | * Calls all registered event listeners for specific event. 134 | * 135 | * @param {string} type 136 | * Type of event. Use `EVENT_TYPE`. 137 | */ 138 | function callEventListener(type) { 139 | const listeners = EVENT_LISTENERS[type]; 140 | 141 | if (!listeners) { 142 | return; 143 | } 144 | 145 | for (const listener of listeners) { 146 | try { 147 | listener({ 148 | type: type 149 | }); 150 | } catch (error) { 151 | console.error(error); 152 | } 153 | } 154 | } 155 | 156 | 157 | main(); 158 | -------------------------------------------------------------------------------- /src/press-on.js: -------------------------------------------------------------------------------- 1 | import {keypress, keydown} from './common/keyboard-event'; 2 | import getTextEventTarget from './get-text-event-target'; 3 | 4 | 5 | //#region Base 6 | 7 | /** 8 | * Imitates physical press on single character. 9 | */ 10 | export function Character(char, { 11 | ctrlKey = false, 12 | shiftKey = false 13 | } = {}) { 14 | // Google Docs handles `keydown` event in case of 15 | // "ctrl" or "shift" modificators, otherwise `keypress` 16 | // event should be used for normal characters 17 | if (ctrlKey || shiftKey) { 18 | keydown( 19 | getTextEventTarget(), 20 | char, 21 | null, 22 | null, 23 | { 24 | ctrlKey, 25 | shiftKey 26 | } 27 | ); 28 | } else { 29 | keypress( 30 | getTextEventTarget(), 31 | char 32 | ); 33 | } 34 | } 35 | 36 | /** 37 | * Imitates physical press on "Backspace". 38 | * 39 | * @param {boolean} ctrlKey 40 | */ 41 | export function Backspace({ 42 | ctrlKey = false 43 | } = {}) { 44 | keydown( 45 | getTextEventTarget(), 46 | 'Backspace', 47 | 'Backspace', 48 | 8, 49 | { 50 | ctrlKey 51 | } 52 | ); 53 | } 54 | 55 | /** 56 | * Imitates physical press on "Tab". 57 | */ 58 | export function Tab() { 59 | keydown( 60 | getTextEventTarget(), 61 | 'Tab', 62 | 'Tab', 63 | 9 64 | ); 65 | } 66 | 67 | /** 68 | * Imitates physical press on "Enter". 69 | */ 70 | export function Enter() { 71 | keydown( 72 | getTextEventTarget(), 73 | 'Enter', 74 | 'Enter', 75 | 13 76 | ); 77 | } 78 | 79 | /** 80 | * Imitates physical press on space character. 81 | */ 82 | export function Space() { 83 | keypress( 84 | getTextEventTarget(), 85 | '\u0020', 86 | 'Space', 87 | 32 88 | ); 89 | } 90 | 91 | /** 92 | * Imitates physical press on "End" button. 93 | */ 94 | export function End({ 95 | ctrlKey = false, 96 | shiftKey = false 97 | } = {}) { 98 | keydown( 99 | getTextEventTarget(), 100 | 'End', 101 | 'End', 102 | 35, 103 | { 104 | ctrlKey, 105 | shiftKey 106 | } 107 | ); 108 | } 109 | 110 | /** 111 | * Imitates physical press on "Home" button. 112 | */ 113 | export function Home({ 114 | ctrlKey = false, 115 | shiftKey = false 116 | } = {}) { 117 | keydown( 118 | getTextEventTarget(), 119 | 'Home', 120 | 'Home', 121 | 36, 122 | { 123 | ctrlKey, 124 | shiftKey 125 | } 126 | ); 127 | } 128 | 129 | /** 130 | * Imitates physical press on left arrow. 131 | */ 132 | export function ArrowLeft({ 133 | ctrlKey = false, 134 | shiftKey = false 135 | } = {}) { 136 | keydown( 137 | getTextEventTarget(), 138 | 'ArrowLeft', 139 | 'ArrowLeft', 140 | 37, 141 | { 142 | ctrlKey, 143 | shiftKey 144 | } 145 | ); 146 | } 147 | 148 | /** 149 | * Imitates physical press on up arrow. 150 | */ 151 | export function ArrowUp({ 152 | ctrlKey = false, 153 | shiftKey = false 154 | } = {}) { 155 | keydown( 156 | getTextEventTarget(), 157 | 'ArrowUp', 158 | 'ArrowUp', 159 | 38, 160 | { 161 | ctrlKey, 162 | shiftKey 163 | } 164 | ); 165 | } 166 | 167 | /** 168 | * Imitates physical press on right arrow. 169 | */ 170 | export function ArrowRight({ 171 | ctrlKey = false, 172 | shiftKey = false 173 | } = {}) { 174 | keydown( 175 | getTextEventTarget(), 176 | 'ArrowRight', 177 | 'ArrowRight', 178 | 39, 179 | { 180 | ctrlKey, 181 | shiftKey 182 | } 183 | ); 184 | } 185 | 186 | /** 187 | * Imitates physical press on down arrow. 188 | */ 189 | export function ArrowDown({ 190 | ctrlKey = false, 191 | shiftKey = false 192 | } = {}) { 193 | keydown( 194 | getTextEventTarget(), 195 | 'ArrowDown', 196 | 'ArrowDown', 197 | 40, 198 | { 199 | ctrlKey, 200 | shiftKey 201 | } 202 | ); 203 | } 204 | 205 | /** 206 | * Imitates physical press on "Delete" ("Del"). 207 | */ 208 | export function Delete({ 209 | ctrlKey = false 210 | } = {}) { 211 | keydown( 212 | getTextEventTarget(), 213 | 'Delete', 214 | 'Delete', 215 | 46, 216 | { 217 | ctrlKey 218 | } 219 | ); 220 | } 221 | 222 | //#endregion 223 | 224 | 225 | //#region Dependence 226 | 227 | /** 228 | * Imitates physical press on "Undo" button. 229 | */ 230 | export function Undo() { 231 | Character('z', { 232 | ctrlKey: true 233 | }); 234 | } 235 | 236 | /** 237 | * Imitates physical press on "Redo" button. 238 | */ 239 | export function Redo() { 240 | Character('y', { 241 | ctrlKey: true 242 | }); 243 | } 244 | 245 | /** 246 | * Imitates physical press on "Print" button 247 | * (print dialog, not print of character). 248 | */ 249 | export function PrintDialog() { 250 | Character('p', { 251 | ctrlKey: true 252 | }); 253 | } 254 | 255 | /** 256 | * Imitates physical press on "Bold" button. 257 | */ 258 | export function Bold() { 259 | Character('b', { 260 | ctrlKey: true 261 | }); 262 | } 263 | 264 | /** 265 | * Imitates physical press on "Italic" button. 266 | */ 267 | export function Italic() { 268 | Character('i', { 269 | ctrlKey: true 270 | }); 271 | } 272 | 273 | /** 274 | * Imitates physical press on "Underline" button. 275 | */ 276 | export function Underline() { 277 | Character('u', { 278 | ctrlKey: true 279 | }); 280 | } 281 | 282 | //#endregion 283 | -------------------------------------------------------------------------------- /src/remove.js: -------------------------------------------------------------------------------- 1 | import * as pressOn from './press-on'; 2 | import isTextSelected from './is-text-selected'; 3 | 4 | 5 | /** 6 | * Removes: 7 | * - if prev word is present, then it will be removed 8 | * - else content from current line will be divided with prev line 9 | */ 10 | export function PrevWord() { 11 | pressOn.Backspace({ 12 | ctrlKey: true 13 | }); 14 | } 15 | 16 | 17 | /** 18 | * Removes: 19 | * - if next word is present, then it will be removed 20 | * - else content from current line will be divided with next line 21 | */ 22 | export function NextWord() { 23 | pressOn.Delete({ 24 | ctrlKey: true 25 | }); 26 | } 27 | 28 | 29 | /** 30 | * Removes active selection. 31 | * 32 | * @returns {boolean} 33 | * `true` - selection was removed, 34 | * `false` - nothing to remove (nothing is selected) 35 | */ 36 | export function Selection() { 37 | if (!isTextSelected()) { 38 | return false; 39 | } 40 | 41 | // "Delete" should be used, not "Backspace". 42 | pressOn.Delete(); 43 | 44 | return true; 45 | } 46 | -------------------------------------------------------------------------------- /src/select.js: -------------------------------------------------------------------------------- 1 | import * as pressOn from './press-on'; 2 | import focusDocument from './focus-document'; 3 | 4 | 5 | /** 6 | * Selects text of entire document. 7 | */ 8 | export function All() { 9 | pressOn.Character('a', { 10 | ctrlKey: true 11 | }); 12 | } 13 | 14 | 15 | /** 16 | * Selects a character that is placed to the left of 17 | * current cursor position. Following logic will be used, 18 | * with priority of actions from top to bottom: 19 | * - if at least one character already selected with reverse selection 20 | * (opposite direction), then lastly selected character will be deselected 21 | * - if at least one character already selected, then next one will 22 | * be selected. If that next character located on previous line, 23 | * than that previous line will be used 24 | * - if nothing selected, then first character will be selected 25 | */ 26 | export function PrevCharacter() { 27 | pressOn.ArrowLeft({ 28 | shiftKey: true 29 | }); 30 | } 31 | 32 | 33 | /** 34 | * Selects a character that is placed to the right of 35 | * current cursor position. Following logic will be used, 36 | * with priority of actions from top to bottom: 37 | * - if at least one character already selected with reverse selection 38 | * (opposite direction), then lastly selected character will be deselected 39 | * - if at least one character already selected, then next one will 40 | * be selected. If that next character located on next line, 41 | * than that next line will be used 42 | * - if nothing selected, then first character will be selected 43 | */ 44 | export function NextCharacter() { 45 | pressOn.ArrowRight({ 46 | shiftKey: true 47 | }); 48 | } 49 | 50 | 51 | /** 52 | * Same as `PrevCharacter`, but performs an action with word. 53 | */ 54 | export function PrevWord() { 55 | pressOn.ArrowLeft({ 56 | shiftKey: true, 57 | ctrlKey: true 58 | }); 59 | } 60 | 61 | 62 | /** 63 | * Same as `NextCharacter`, but performs an action with word. 64 | */ 65 | export function NextWord() { 66 | pressOn.ArrowRight({ 67 | shiftKey: true, 68 | ctrlKey: true 69 | }); 70 | } 71 | 72 | 73 | /** 74 | * Selects N number of characters to the left where N 75 | * is a max length of line. 76 | */ 77 | export function PrevLine() { 78 | // requires focus to behave correctly 79 | focusDocument(); 80 | 81 | pressOn.ArrowUp({ 82 | shiftKey: true 83 | }); 84 | } 85 | 86 | 87 | /** 88 | * Same as `PrevLine`, but uses right direction. 89 | */ 90 | export function NextLine() { 91 | // requires focus to behave correctly 92 | focusDocument(); 93 | 94 | pressOn.ArrowDown({ 95 | shiftKey: true 96 | }); 97 | } 98 | 99 | 100 | /** 101 | * Selects a paragraph that is placed to the left of 102 | * current cursor position. Following logic will be used, 103 | * with priority of actions from top to bottom: 104 | * - if it is start of current paragraph, then previous 105 | * paragraph will be selected 106 | * - else text between current paragraph start and current 107 | * cursor position will be selected 108 | */ 109 | export function PrevParagraph() { 110 | pressOn.ArrowUp({ 111 | shiftKey: true, 112 | ctrlKey: true 113 | }); 114 | } 115 | 116 | 117 | /** 118 | * Selects a paragraph that is placed to the right of 119 | * current cursor position. Following logic will be used, 120 | * with priority of actions from top to bottom: 121 | * - if it is end of current paragraph, then next 122 | * paragraph will be NOT selected 123 | * - else text between current paragraph end and current 124 | * cursor position will be selected 125 | */ 126 | export function NextParagraph() { 127 | pressOn.ArrowDown({ 128 | shiftKey: true, 129 | ctrlKey: true 130 | }); 131 | } 132 | 133 | 134 | /** 135 | * Selects a text between current cursor position and 136 | * current line start. 137 | */ 138 | export function TextBetweenCursorAndLineStart() { 139 | // requires focus to behave correctly 140 | focusDocument(); 141 | 142 | pressOn.Home({ 143 | shiftKey: true 144 | }); 145 | } 146 | 147 | 148 | /** 149 | * Same as `TextBetweenCursorAndLineStart`, but interacts 150 | * with current line end. 151 | */ 152 | export function TextBetweenCursorAndLineEnd() { 153 | // requires focus to behave correctly 154 | focusDocument(); 155 | 156 | pressOn.End({ 157 | shiftKey: true 158 | }); 159 | } 160 | 161 | 162 | /** 163 | * Same as `TextBetweenCursorAndLineStart`, but interacts 164 | * with document start. 165 | */ 166 | export function TextBetweenCursorAndDocumentStart() { 167 | pressOn.Home({ 168 | shiftKey: true, 169 | ctrlKey: true 170 | }); 171 | } 172 | 173 | 174 | /** 175 | * Same as `TextBetweenCursorAndLineStart`, but interacts 176 | * with document end. 177 | */ 178 | export function TextBetweenCursorAndDocumentEnd() { 179 | pressOn.End({ 180 | shiftKey: true, 181 | ctrlKey: true 182 | }); 183 | } 184 | -------------------------------------------------------------------------------- /src/type-text.js: -------------------------------------------------------------------------------- 1 | import * as pressOn from './press-on'; 2 | 3 | 4 | /** 5 | * Types text at current caret position. 6 | * 7 | * - imitates physical typing 8 | * 9 | * @param {string} text 10 | * Text to type. 11 | */ 12 | export default function typeText(text) { 13 | type(text); 14 | } 15 | 16 | 17 | /** 18 | * Types text at current caret position. 19 | * 20 | * - imitates key press char by char, 21 | * can take a long time for long text. 22 | * 23 | * @param {string} text 24 | */ 25 | function type(text) { 26 | for (const char of text) { 27 | pressOn.Character(char); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/inject.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | 4 | /** 5 | * Call this function in developer console of page to 6 | * inject local package into Google Docs page. 7 | * 8 | * - requires HTTP server to be executed. 9 | * - if you want to refresh script content, 10 | * just call this function again. 11 | * 12 | * If you will see error message about invalid certificate, 13 | * then open script URL in new tab, click on "Proceed to unsafe" 14 | * (or something like that), after that you should see loaded 15 | * script content, and now back to Google Docs and call this 16 | * method again. 17 | */ 18 | function inject() { 19 | const script = document.createElement('script'); 20 | 21 | script.type = 'text/javascript'; 22 | script.src = 'https://127.0.0.1:8080/dist/iife/index.js'; 23 | 24 | document.head.appendChild(script); 25 | } 26 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for google-docs-utils 2.3 2 | // Project: https://github.com/Amaimersion/google-docs-utils/blob/master/README.md 3 | // Definitions by: Sergey Kuznetsov 4 | // Definitions: https://github.com/Amaimersion/google-docs-utils 5 | // TypeScript Version: 4.1 6 | 7 | 8 | //#region Private 9 | 10 | type QuerySelectorResult = T | null; 11 | type QuerySelectorAllResult = T[]; 12 | 13 | type CtrlModificator = { 14 | ctrlKey?: boolean; 15 | }; 16 | type ShiftModificator = { 17 | shiftKey?: boolean; 18 | }; 19 | type CtrlShiftModificator = ( 20 | CtrlModificator & 21 | ShiftModificator 22 | ); 23 | 24 | interface GetSelectionResult { 25 | text: string; 26 | selectedText: string; 27 | selectionStart: number; 28 | selectionEnd: number; 29 | textRect: DOMRectReadOnly; 30 | selectionRect: DOMRectReadOnly; 31 | textElement: HTMLElement; 32 | selectionElement: HTMLElement; 33 | } 34 | 35 | interface GetCaretResult { 36 | element: HTMLElement; 37 | wordElement: HTMLElement; 38 | lineIndex: number; 39 | positionIndexRelativeToWord: number; 40 | } 41 | 42 | interface GetCaretWordResult { 43 | word: string; 44 | text: string; 45 | indexStart: number; 46 | indexEnd: number; 47 | } 48 | 49 | interface GoogleDocsEvent { 50 | type: string; 51 | } 52 | 53 | //#endregion 54 | 55 | 56 | //#region Public 57 | 58 | export function getEditorElement(): QuerySelectorResult; 59 | 60 | export function getTextEventTarget(): QuerySelectorResult; 61 | 62 | export function getPagesElements(): QuerySelectorAllResult; 63 | 64 | export function getLinesElements(): QuerySelectorAllResult; 65 | 66 | export function getLinesTextElements(): QuerySelectorAllResult; 67 | 68 | export function getLinesText(): string[]; 69 | 70 | export function getLineText( 71 | lineIndex: number, 72 | startIndex?: number, 73 | endIndex?: number 74 | ): string | null; 75 | 76 | export function clearTextContent(textContent: string): string; 77 | 78 | export function getWordElements(): QuerySelectorAllResult[]; 79 | 80 | export function getSelectionOverlayElements(): QuerySelectorAllResult; 81 | 82 | export function getSelection(): Array>; 83 | 84 | export function getCursorElement(): QuerySelectorResult; 85 | 86 | export function getActiveCursorElement(): QuerySelectorResult; 87 | 88 | export function getCaretElement(): QuerySelectorResult; 89 | 90 | export function getCaret(): GetCaretResult | null; 91 | 92 | export function getCaretWord(): GetCaretWordResult | null; 93 | 94 | export const pressOn: { 95 | Character: (char: string, modificator?: CtrlShiftModificator) => void, 96 | Space: () => void, 97 | Delete: (modificator?: CtrlModificator) => void, 98 | Backspace: (modificator?: CtrlModificator) => void, 99 | Enter: () => void, 100 | Tab: () => void, 101 | ArrowLeft: (modificator?: CtrlShiftModificator) => void, 102 | ArrowRight: (modificator?: CtrlShiftModificator) => void, 103 | ArrowUp: (modificator?: CtrlShiftModificator) => void, 104 | ArrowDown: (modificator?: CtrlShiftModificator) => void, 105 | Undo: () => void, 106 | Redo: () => void, 107 | PrintDialog: () => void, 108 | End: (modificator?: CtrlShiftModificator) => void, 109 | Home: (modificator?: CtrlShiftModificator) => void, 110 | Bold: () => void, 111 | Italic: () => void, 112 | Underline: () => void 113 | }; 114 | 115 | export function typeText(text: string): void; 116 | 117 | export function isTextSelected(): boolean; 118 | 119 | export function isDocumentActive(): boolean; 120 | 121 | export function focusDocument(): boolean; 122 | 123 | export const remove: { 124 | PrevWord: () => void, 125 | NextWord: () => void, 126 | Selection: () => boolean 127 | }; 128 | 129 | export const moveCursorTo: { 130 | PrevCharacter: () => void, 131 | NextCharacter: () => void, 132 | PrevLine: () => void, 133 | NextLine: () => void, 134 | PrevWord: () => void, 135 | NextWord: () => void, 136 | PrevParagraph: () => void, 137 | NextParagraph: () => void, 138 | LineStart: () => void, 139 | LineEnd: () => void, 140 | DocumentStart: () => void, 141 | DocumentEnd: () => void 142 | }; 143 | 144 | export const select: { 145 | All: () => void, 146 | PrevCharacter: () => void, 147 | NextCharacter: () => void, 148 | PrevWord: () => void, 149 | NextWord: () => void, 150 | PrevLine: () => void, 151 | NextLine: () => void, 152 | PrevParagraph: () => void, 153 | NextParagraph: () => void, 154 | TextBetweenCursorAndLineStart: () => void, 155 | TextBetweenCursorAndLineEnd: () => void, 156 | TextBetweenCursorAndDocumentStart: () => void, 157 | TextBetweenCursorAndDocumentEnd: () => void 158 | }; 159 | 160 | export function addEventListener( 161 | type: string, 162 | listener: (event: GoogleDocsEvent) => any 163 | ): void; 164 | 165 | //#endregion 166 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.11": 6 | version "7.12.11" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" 8 | integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== 9 | dependencies: 10 | "@babel/highlight" "^7.10.4" 11 | 12 | "@babel/generator@^7.12.11": 13 | version "7.12.11" 14 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" 15 | integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== 16 | dependencies: 17 | "@babel/types" "^7.12.11" 18 | jsesc "^2.5.1" 19 | source-map "^0.5.0" 20 | 21 | "@babel/helper-function-name@^7.12.11": 22 | version "7.12.11" 23 | resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" 24 | integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== 25 | dependencies: 26 | "@babel/helper-get-function-arity" "^7.12.10" 27 | "@babel/template" "^7.12.7" 28 | "@babel/types" "^7.12.11" 29 | 30 | "@babel/helper-get-function-arity@^7.12.10": 31 | version "7.12.10" 32 | resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" 33 | integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== 34 | dependencies: 35 | "@babel/types" "^7.12.10" 36 | 37 | "@babel/helper-split-export-declaration@^7.12.11": 38 | version "7.12.11" 39 | resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" 40 | integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== 41 | dependencies: 42 | "@babel/types" "^7.12.11" 43 | 44 | "@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": 45 | version "7.12.11" 46 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" 47 | integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== 48 | 49 | "@babel/highlight@^7.10.4": 50 | version "7.10.4" 51 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" 52 | integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== 53 | dependencies: 54 | "@babel/helper-validator-identifier" "^7.10.4" 55 | chalk "^2.0.0" 56 | js-tokens "^4.0.0" 57 | 58 | "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0": 59 | version "7.12.11" 60 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" 61 | integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== 62 | 63 | "@babel/template@^7.12.7": 64 | version "7.12.7" 65 | resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" 66 | integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== 67 | dependencies: 68 | "@babel/code-frame" "^7.10.4" 69 | "@babel/parser" "^7.12.7" 70 | "@babel/types" "^7.12.7" 71 | 72 | "@babel/traverse@^7.7.0": 73 | version "7.12.12" 74 | resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" 75 | integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== 76 | dependencies: 77 | "@babel/code-frame" "^7.12.11" 78 | "@babel/generator" "^7.12.11" 79 | "@babel/helper-function-name" "^7.12.11" 80 | "@babel/helper-split-export-declaration" "^7.12.11" 81 | "@babel/parser" "^7.12.11" 82 | "@babel/types" "^7.12.12" 83 | debug "^4.1.0" 84 | globals "^11.1.0" 85 | lodash "^4.17.19" 86 | 87 | "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.7", "@babel/types@^7.7.0": 88 | version "7.12.12" 89 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" 90 | integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== 91 | dependencies: 92 | "@babel/helper-validator-identifier" "^7.12.11" 93 | lodash "^4.17.19" 94 | to-fast-properties "^2.0.0" 95 | 96 | "@eslint/eslintrc@^0.2.2": 97 | version "0.2.2" 98 | resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76" 99 | integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ== 100 | dependencies: 101 | ajv "^6.12.4" 102 | debug "^4.1.1" 103 | espree "^7.3.0" 104 | globals "^12.1.0" 105 | ignore "^4.0.6" 106 | import-fresh "^3.2.1" 107 | js-yaml "^3.13.1" 108 | lodash "^4.17.19" 109 | minimatch "^3.0.4" 110 | strip-json-comments "^3.1.1" 111 | 112 | "@types/node@*": 113 | version "14.14.14" 114 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" 115 | integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== 116 | 117 | acorn-jsx@^5.3.1: 118 | version "5.3.1" 119 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" 120 | integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== 121 | 122 | acorn@^7.4.0: 123 | version "7.4.1" 124 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" 125 | integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== 126 | 127 | ajv@^6.10.0, ajv@^6.12.4: 128 | version "6.12.6" 129 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" 130 | integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== 131 | dependencies: 132 | fast-deep-equal "^3.1.1" 133 | fast-json-stable-stringify "^2.0.0" 134 | json-schema-traverse "^0.4.1" 135 | uri-js "^4.2.2" 136 | 137 | ansi-colors@^4.1.1: 138 | version "4.1.1" 139 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" 140 | integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== 141 | 142 | ansi-regex@^5.0.0: 143 | version "5.0.0" 144 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" 145 | integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== 146 | 147 | ansi-styles@^3.2.1: 148 | version "3.2.1" 149 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 150 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 151 | dependencies: 152 | color-convert "^1.9.0" 153 | 154 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 155 | version "4.3.0" 156 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 157 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 158 | dependencies: 159 | color-convert "^2.0.1" 160 | 161 | argparse@^1.0.7: 162 | version "1.0.10" 163 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" 164 | integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== 165 | dependencies: 166 | sprintf-js "~1.0.2" 167 | 168 | astral-regex@^2.0.0: 169 | version "2.0.0" 170 | resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" 171 | integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== 172 | 173 | async@^2.6.2: 174 | version "2.6.3" 175 | resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" 176 | integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== 177 | dependencies: 178 | lodash "^4.17.14" 179 | 180 | babel-eslint@^10.1.0: 181 | version "10.1.0" 182 | resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" 183 | integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== 184 | dependencies: 185 | "@babel/code-frame" "^7.0.0" 186 | "@babel/parser" "^7.7.0" 187 | "@babel/traverse" "^7.7.0" 188 | "@babel/types" "^7.7.0" 189 | eslint-visitor-keys "^1.0.0" 190 | resolve "^1.12.0" 191 | 192 | balanced-match@^1.0.0: 193 | version "1.0.0" 194 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 195 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 196 | 197 | basic-auth@^1.0.3: 198 | version "1.1.0" 199 | resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" 200 | integrity sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ= 201 | 202 | brace-expansion@^1.1.7: 203 | version "1.1.11" 204 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 205 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 206 | dependencies: 207 | balanced-match "^1.0.0" 208 | concat-map "0.0.1" 209 | 210 | buffer-from@^1.0.0: 211 | version "1.1.1" 212 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 213 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 214 | 215 | callsites@^3.0.0: 216 | version "3.1.0" 217 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 218 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 219 | 220 | chalk@^2.0.0: 221 | version "2.4.2" 222 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 223 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 224 | dependencies: 225 | ansi-styles "^3.2.1" 226 | escape-string-regexp "^1.0.5" 227 | supports-color "^5.3.0" 228 | 229 | chalk@^4.0.0: 230 | version "4.1.0" 231 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" 232 | integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== 233 | dependencies: 234 | ansi-styles "^4.1.0" 235 | supports-color "^7.1.0" 236 | 237 | color-convert@^1.9.0: 238 | version "1.9.3" 239 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 240 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 241 | dependencies: 242 | color-name "1.1.3" 243 | 244 | color-convert@^2.0.1: 245 | version "2.0.1" 246 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 247 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 248 | dependencies: 249 | color-name "~1.1.4" 250 | 251 | color-name@1.1.3: 252 | version "1.1.3" 253 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 254 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 255 | 256 | color-name@~1.1.4: 257 | version "1.1.4" 258 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 259 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 260 | 261 | colors@^1.4.0: 262 | version "1.4.0" 263 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" 264 | integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== 265 | 266 | commander@^2.20.0: 267 | version "2.20.3" 268 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 269 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 270 | 271 | concat-map@0.0.1: 272 | version "0.0.1" 273 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 274 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 275 | 276 | corser@^2.0.1: 277 | version "2.0.1" 278 | resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" 279 | integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c= 280 | 281 | cross-spawn@^7.0.2: 282 | version "7.0.3" 283 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 284 | integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 285 | dependencies: 286 | path-key "^3.1.0" 287 | shebang-command "^2.0.0" 288 | which "^2.0.1" 289 | 290 | debug@^3.1.1: 291 | version "3.2.7" 292 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" 293 | integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== 294 | dependencies: 295 | ms "^2.1.1" 296 | 297 | debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: 298 | version "4.3.1" 299 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" 300 | integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== 301 | dependencies: 302 | ms "2.1.2" 303 | 304 | deep-is@^0.1.3: 305 | version "0.1.3" 306 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 307 | integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= 308 | 309 | doctrine@^3.0.0: 310 | version "3.0.0" 311 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" 312 | integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== 313 | dependencies: 314 | esutils "^2.0.2" 315 | 316 | ecstatic@^3.3.2: 317 | version "3.3.2" 318 | resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48" 319 | integrity sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog== 320 | dependencies: 321 | he "^1.1.1" 322 | mime "^1.6.0" 323 | minimist "^1.1.0" 324 | url-join "^2.0.5" 325 | 326 | emoji-regex@^8.0.0: 327 | version "8.0.0" 328 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 329 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 330 | 331 | enquirer@^2.3.5: 332 | version "2.3.6" 333 | resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" 334 | integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== 335 | dependencies: 336 | ansi-colors "^4.1.1" 337 | 338 | escape-string-regexp@^1.0.5: 339 | version "1.0.5" 340 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 341 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 342 | 343 | eslint-scope@^5.1.1: 344 | version "5.1.1" 345 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" 346 | integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== 347 | dependencies: 348 | esrecurse "^4.3.0" 349 | estraverse "^4.1.1" 350 | 351 | eslint-utils@^2.1.0: 352 | version "2.1.0" 353 | resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" 354 | integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== 355 | dependencies: 356 | eslint-visitor-keys "^1.1.0" 357 | 358 | eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: 359 | version "1.3.0" 360 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" 361 | integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== 362 | 363 | eslint-visitor-keys@^2.0.0: 364 | version "2.0.0" 365 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" 366 | integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== 367 | 368 | eslint@^7.16.0: 369 | version "7.16.0" 370 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.16.0.tgz#a761605bf9a7b32d24bb7cde59aeb0fd76f06092" 371 | integrity sha512-iVWPS785RuDA4dWuhhgXTNrGxHHK3a8HLSMBgbbU59ruJDubUraXN8N5rn7kb8tG6sjg74eE0RA3YWT51eusEw== 372 | dependencies: 373 | "@babel/code-frame" "^7.0.0" 374 | "@eslint/eslintrc" "^0.2.2" 375 | ajv "^6.10.0" 376 | chalk "^4.0.0" 377 | cross-spawn "^7.0.2" 378 | debug "^4.0.1" 379 | doctrine "^3.0.0" 380 | enquirer "^2.3.5" 381 | eslint-scope "^5.1.1" 382 | eslint-utils "^2.1.0" 383 | eslint-visitor-keys "^2.0.0" 384 | espree "^7.3.1" 385 | esquery "^1.2.0" 386 | esutils "^2.0.2" 387 | file-entry-cache "^6.0.0" 388 | functional-red-black-tree "^1.0.1" 389 | glob-parent "^5.0.0" 390 | globals "^12.1.0" 391 | ignore "^4.0.6" 392 | import-fresh "^3.0.0" 393 | imurmurhash "^0.1.4" 394 | is-glob "^4.0.0" 395 | js-yaml "^3.13.1" 396 | json-stable-stringify-without-jsonify "^1.0.1" 397 | levn "^0.4.1" 398 | lodash "^4.17.19" 399 | minimatch "^3.0.4" 400 | natural-compare "^1.4.0" 401 | optionator "^0.9.1" 402 | progress "^2.0.0" 403 | regexpp "^3.1.0" 404 | semver "^7.2.1" 405 | strip-ansi "^6.0.0" 406 | strip-json-comments "^3.1.0" 407 | table "^6.0.4" 408 | text-table "^0.2.0" 409 | v8-compile-cache "^2.0.3" 410 | 411 | espree@^7.3.0, espree@^7.3.1: 412 | version "7.3.1" 413 | resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" 414 | integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== 415 | dependencies: 416 | acorn "^7.4.0" 417 | acorn-jsx "^5.3.1" 418 | eslint-visitor-keys "^1.3.0" 419 | 420 | esprima@^4.0.0: 421 | version "4.0.1" 422 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 423 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 424 | 425 | esquery@^1.2.0: 426 | version "1.3.1" 427 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" 428 | integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== 429 | dependencies: 430 | estraverse "^5.1.0" 431 | 432 | esrecurse@^4.3.0: 433 | version "4.3.0" 434 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" 435 | integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== 436 | dependencies: 437 | estraverse "^5.2.0" 438 | 439 | estraverse@^4.1.1: 440 | version "4.3.0" 441 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" 442 | integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== 443 | 444 | estraverse@^5.1.0, estraverse@^5.2.0: 445 | version "5.2.0" 446 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" 447 | integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== 448 | 449 | esutils@^2.0.2: 450 | version "2.0.3" 451 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 452 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 453 | 454 | eventemitter3@^4.0.0: 455 | version "4.0.7" 456 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" 457 | integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== 458 | 459 | fast-deep-equal@^3.1.1: 460 | version "3.1.3" 461 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 462 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 463 | 464 | fast-json-stable-stringify@^2.0.0: 465 | version "2.1.0" 466 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 467 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 468 | 469 | fast-levenshtein@^2.0.6: 470 | version "2.0.6" 471 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 472 | integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= 473 | 474 | file-entry-cache@^6.0.0: 475 | version "6.0.0" 476 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" 477 | integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA== 478 | dependencies: 479 | flat-cache "^3.0.4" 480 | 481 | flat-cache@^3.0.4: 482 | version "3.0.4" 483 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" 484 | integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== 485 | dependencies: 486 | flatted "^3.1.0" 487 | rimraf "^3.0.2" 488 | 489 | flatted@^3.1.0: 490 | version "3.1.0" 491 | resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067" 492 | integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA== 493 | 494 | follow-redirects@^1.0.0: 495 | version "1.13.1" 496 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" 497 | integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== 498 | 499 | fs.realpath@^1.0.0: 500 | version "1.0.0" 501 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 502 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 503 | 504 | fsevents@~2.1.2: 505 | version "2.1.3" 506 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" 507 | integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== 508 | 509 | function-bind@^1.1.1: 510 | version "1.1.1" 511 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 512 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 513 | 514 | functional-red-black-tree@^1.0.1: 515 | version "1.0.1" 516 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 517 | integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= 518 | 519 | glob-parent@^5.0.0: 520 | version "5.1.1" 521 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" 522 | integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== 523 | dependencies: 524 | is-glob "^4.0.1" 525 | 526 | glob@^7.1.3: 527 | version "7.1.6" 528 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 529 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 530 | dependencies: 531 | fs.realpath "^1.0.0" 532 | inflight "^1.0.4" 533 | inherits "2" 534 | minimatch "^3.0.4" 535 | once "^1.3.0" 536 | path-is-absolute "^1.0.0" 537 | 538 | globals@^11.1.0: 539 | version "11.12.0" 540 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" 541 | integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== 542 | 543 | globals@^12.1.0: 544 | version "12.4.0" 545 | resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" 546 | integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== 547 | dependencies: 548 | type-fest "^0.8.1" 549 | 550 | has-flag@^3.0.0: 551 | version "3.0.0" 552 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 553 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 554 | 555 | has-flag@^4.0.0: 556 | version "4.0.0" 557 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 558 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 559 | 560 | has@^1.0.3: 561 | version "1.0.3" 562 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 563 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 564 | dependencies: 565 | function-bind "^1.1.1" 566 | 567 | he@^1.1.1: 568 | version "1.2.0" 569 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 570 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 571 | 572 | http-proxy@^1.18.0: 573 | version "1.18.1" 574 | resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" 575 | integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== 576 | dependencies: 577 | eventemitter3 "^4.0.0" 578 | follow-redirects "^1.0.0" 579 | requires-port "^1.0.0" 580 | 581 | http-server@^0.12.3: 582 | version "0.12.3" 583 | resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.12.3.tgz#ba0471d0ecc425886616cb35c4faf279140a0d37" 584 | integrity sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA== 585 | dependencies: 586 | basic-auth "^1.0.3" 587 | colors "^1.4.0" 588 | corser "^2.0.1" 589 | ecstatic "^3.3.2" 590 | http-proxy "^1.18.0" 591 | minimist "^1.2.5" 592 | opener "^1.5.1" 593 | portfinder "^1.0.25" 594 | secure-compare "3.0.1" 595 | union "~0.5.0" 596 | 597 | ignore@^4.0.6: 598 | version "4.0.6" 599 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" 600 | integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== 601 | 602 | import-fresh@^3.0.0, import-fresh@^3.2.1: 603 | version "3.3.0" 604 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 605 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 606 | dependencies: 607 | parent-module "^1.0.0" 608 | resolve-from "^4.0.0" 609 | 610 | imurmurhash@^0.1.4: 611 | version "0.1.4" 612 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 613 | integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 614 | 615 | inflight@^1.0.4: 616 | version "1.0.6" 617 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 618 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 619 | dependencies: 620 | once "^1.3.0" 621 | wrappy "1" 622 | 623 | inherits@2: 624 | version "2.0.4" 625 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 626 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 627 | 628 | is-core-module@^2.1.0: 629 | version "2.2.0" 630 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" 631 | integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== 632 | dependencies: 633 | has "^1.0.3" 634 | 635 | is-extglob@^2.1.1: 636 | version "2.1.1" 637 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 638 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 639 | 640 | is-fullwidth-code-point@^3.0.0: 641 | version "3.0.0" 642 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 643 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 644 | 645 | is-glob@^4.0.0, is-glob@^4.0.1: 646 | version "4.0.1" 647 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 648 | integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 649 | dependencies: 650 | is-extglob "^2.1.1" 651 | 652 | isexe@^2.0.0: 653 | version "2.0.0" 654 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 655 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 656 | 657 | jest-worker@^26.2.1: 658 | version "26.6.2" 659 | resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" 660 | integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== 661 | dependencies: 662 | "@types/node" "*" 663 | merge-stream "^2.0.0" 664 | supports-color "^7.0.0" 665 | 666 | js-tokens@^4.0.0: 667 | version "4.0.0" 668 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 669 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 670 | 671 | js-yaml@^3.13.1: 672 | version "3.14.1" 673 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" 674 | integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== 675 | dependencies: 676 | argparse "^1.0.7" 677 | esprima "^4.0.0" 678 | 679 | jsesc@^2.5.1: 680 | version "2.5.2" 681 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" 682 | integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== 683 | 684 | json-schema-traverse@^0.4.1: 685 | version "0.4.1" 686 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 687 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 688 | 689 | json-stable-stringify-without-jsonify@^1.0.1: 690 | version "1.0.1" 691 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 692 | integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= 693 | 694 | levn@^0.4.1: 695 | version "0.4.1" 696 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" 697 | integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== 698 | dependencies: 699 | prelude-ls "^1.2.1" 700 | type-check "~0.4.0" 701 | 702 | lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20: 703 | version "4.17.20" 704 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" 705 | integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== 706 | 707 | lru-cache@^6.0.0: 708 | version "6.0.0" 709 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" 710 | integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== 711 | dependencies: 712 | yallist "^4.0.0" 713 | 714 | merge-stream@^2.0.0: 715 | version "2.0.0" 716 | resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" 717 | integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== 718 | 719 | mime@^1.6.0: 720 | version "1.6.0" 721 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 722 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 723 | 724 | minimatch@^3.0.4: 725 | version "3.0.4" 726 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 727 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 728 | dependencies: 729 | brace-expansion "^1.1.7" 730 | 731 | minimist@^1.1.0, minimist@^1.2.5: 732 | version "1.2.5" 733 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 734 | integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 735 | 736 | mkdirp@^0.5.5: 737 | version "0.5.5" 738 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" 739 | integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== 740 | dependencies: 741 | minimist "^1.2.5" 742 | 743 | ms@2.1.2: 744 | version "2.1.2" 745 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 746 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 747 | 748 | ms@^2.1.1: 749 | version "2.1.3" 750 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 751 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 752 | 753 | natural-compare@^1.4.0: 754 | version "1.4.0" 755 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 756 | integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= 757 | 758 | once@^1.3.0: 759 | version "1.4.0" 760 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 761 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 762 | dependencies: 763 | wrappy "1" 764 | 765 | opener@^1.5.1: 766 | version "1.5.2" 767 | resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" 768 | integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== 769 | 770 | optionator@^0.9.1: 771 | version "0.9.1" 772 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" 773 | integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== 774 | dependencies: 775 | deep-is "^0.1.3" 776 | fast-levenshtein "^2.0.6" 777 | levn "^0.4.1" 778 | prelude-ls "^1.2.1" 779 | type-check "^0.4.0" 780 | word-wrap "^1.2.3" 781 | 782 | parent-module@^1.0.0: 783 | version "1.0.1" 784 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 785 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 786 | dependencies: 787 | callsites "^3.0.0" 788 | 789 | path-is-absolute@^1.0.0: 790 | version "1.0.1" 791 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 792 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 793 | 794 | path-key@^3.1.0: 795 | version "3.1.1" 796 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 797 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 798 | 799 | path-parse@^1.0.6: 800 | version "1.0.6" 801 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 802 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 803 | 804 | portfinder@^1.0.25: 805 | version "1.0.28" 806 | resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" 807 | integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== 808 | dependencies: 809 | async "^2.6.2" 810 | debug "^3.1.1" 811 | mkdirp "^0.5.5" 812 | 813 | prelude-ls@^1.2.1: 814 | version "1.2.1" 815 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" 816 | integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== 817 | 818 | progress@^2.0.0: 819 | version "2.0.3" 820 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 821 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 822 | 823 | punycode@^2.1.0: 824 | version "2.1.1" 825 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 826 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 827 | 828 | qs@^6.4.0: 829 | version "6.9.4" 830 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" 831 | integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== 832 | 833 | randombytes@^2.1.0: 834 | version "2.1.0" 835 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 836 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 837 | dependencies: 838 | safe-buffer "^5.1.0" 839 | 840 | regexpp@^3.1.0: 841 | version "3.1.0" 842 | resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" 843 | integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== 844 | 845 | requires-port@^1.0.0: 846 | version "1.0.0" 847 | resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" 848 | integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= 849 | 850 | resolve-from@^4.0.0: 851 | version "4.0.0" 852 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 853 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 854 | 855 | resolve@^1.12.0: 856 | version "1.19.0" 857 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" 858 | integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== 859 | dependencies: 860 | is-core-module "^2.1.0" 861 | path-parse "^1.0.6" 862 | 863 | rimraf@^3.0.2: 864 | version "3.0.2" 865 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" 866 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 867 | dependencies: 868 | glob "^7.1.3" 869 | 870 | rollup-plugin-terser@^7.0.2: 871 | version "7.0.2" 872 | resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" 873 | integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== 874 | dependencies: 875 | "@babel/code-frame" "^7.10.4" 876 | jest-worker "^26.2.1" 877 | serialize-javascript "^4.0.0" 878 | terser "^5.0.0" 879 | 880 | rollup@^2.35.1: 881 | version "2.35.1" 882 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.35.1.tgz#e6bc8d10893556a638066f89e8c97f422d03968c" 883 | integrity sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA== 884 | optionalDependencies: 885 | fsevents "~2.1.2" 886 | 887 | safe-buffer@^5.1.0: 888 | version "5.2.1" 889 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 890 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 891 | 892 | secure-compare@3.0.1: 893 | version "3.0.1" 894 | resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" 895 | integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM= 896 | 897 | semver@^7.2.1: 898 | version "7.3.4" 899 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" 900 | integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== 901 | dependencies: 902 | lru-cache "^6.0.0" 903 | 904 | serialize-javascript@^4.0.0: 905 | version "4.0.0" 906 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" 907 | integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== 908 | dependencies: 909 | randombytes "^2.1.0" 910 | 911 | shebang-command@^2.0.0: 912 | version "2.0.0" 913 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 914 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 915 | dependencies: 916 | shebang-regex "^3.0.0" 917 | 918 | shebang-regex@^3.0.0: 919 | version "3.0.0" 920 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 921 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 922 | 923 | slice-ansi@^4.0.0: 924 | version "4.0.0" 925 | resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" 926 | integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== 927 | dependencies: 928 | ansi-styles "^4.0.0" 929 | astral-regex "^2.0.0" 930 | is-fullwidth-code-point "^3.0.0" 931 | 932 | source-map-support@~0.5.19: 933 | version "0.5.19" 934 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 935 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 936 | dependencies: 937 | buffer-from "^1.0.0" 938 | source-map "^0.6.0" 939 | 940 | source-map@^0.5.0: 941 | version "0.5.7" 942 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 943 | integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= 944 | 945 | source-map@^0.6.0: 946 | version "0.6.1" 947 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 948 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 949 | 950 | source-map@~0.7.2: 951 | version "0.7.3" 952 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" 953 | integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== 954 | 955 | sprintf-js@~1.0.2: 956 | version "1.0.3" 957 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 958 | integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 959 | 960 | string-width@^4.2.0: 961 | version "4.2.0" 962 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" 963 | integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== 964 | dependencies: 965 | emoji-regex "^8.0.0" 966 | is-fullwidth-code-point "^3.0.0" 967 | strip-ansi "^6.0.0" 968 | 969 | strip-ansi@^6.0.0: 970 | version "6.0.0" 971 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" 972 | integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== 973 | dependencies: 974 | ansi-regex "^5.0.0" 975 | 976 | strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: 977 | version "3.1.1" 978 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 979 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 980 | 981 | supports-color@^5.3.0: 982 | version "5.5.0" 983 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 984 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 985 | dependencies: 986 | has-flag "^3.0.0" 987 | 988 | supports-color@^7.0.0, supports-color@^7.1.0: 989 | version "7.2.0" 990 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 991 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 992 | dependencies: 993 | has-flag "^4.0.0" 994 | 995 | table@^6.0.4: 996 | version "6.0.4" 997 | resolved "https://registry.yarnpkg.com/table/-/table-6.0.4.tgz#c523dd182177e926c723eb20e1b341238188aa0d" 998 | integrity sha512-sBT4xRLdALd+NFBvwOz8bw4b15htyythha+q+DVZqy2RS08PPC8O2sZFgJYEY7bJvbCFKccs+WIZ/cd+xxTWCw== 999 | dependencies: 1000 | ajv "^6.12.4" 1001 | lodash "^4.17.20" 1002 | slice-ansi "^4.0.0" 1003 | string-width "^4.2.0" 1004 | 1005 | terser@^5.0.0: 1006 | version "5.5.1" 1007 | resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289" 1008 | integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ== 1009 | dependencies: 1010 | commander "^2.20.0" 1011 | source-map "~0.7.2" 1012 | source-map-support "~0.5.19" 1013 | 1014 | text-table@^0.2.0: 1015 | version "0.2.0" 1016 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 1017 | integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= 1018 | 1019 | to-fast-properties@^2.0.0: 1020 | version "2.0.0" 1021 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" 1022 | integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= 1023 | 1024 | type-check@^0.4.0, type-check@~0.4.0: 1025 | version "0.4.0" 1026 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" 1027 | integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== 1028 | dependencies: 1029 | prelude-ls "^1.2.1" 1030 | 1031 | type-fest@^0.8.1: 1032 | version "0.8.1" 1033 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" 1034 | integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== 1035 | 1036 | union@~0.5.0: 1037 | version "0.5.0" 1038 | resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" 1039 | integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== 1040 | dependencies: 1041 | qs "^6.4.0" 1042 | 1043 | uri-js@^4.2.2: 1044 | version "4.4.0" 1045 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" 1046 | integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== 1047 | dependencies: 1048 | punycode "^2.1.0" 1049 | 1050 | url-join@^2.0.5: 1051 | version "2.0.5" 1052 | resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" 1053 | integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= 1054 | 1055 | v8-compile-cache@^2.0.3: 1056 | version "2.2.0" 1057 | resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" 1058 | integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== 1059 | 1060 | which@^2.0.1: 1061 | version "2.0.2" 1062 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 1063 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1064 | dependencies: 1065 | isexe "^2.0.0" 1066 | 1067 | word-wrap@^1.2.3: 1068 | version "1.2.3" 1069 | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" 1070 | integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== 1071 | 1072 | wrappy@1: 1073 | version "1.0.2" 1074 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1075 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1076 | 1077 | yallist@^4.0.0: 1078 | version "4.0.0" 1079 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" 1080 | integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 1081 | --------------------------------------------------------------------------------