Use Electron Spellchecker

This commit is contained in:
Ken Powers 2020-03-20 17:00:11 -04:00 committed by Scott Nonnenberg
parent 42152be4af
commit 4a8f5db0a4
15 changed files with 195 additions and 381 deletions

View file

@ -147,6 +147,14 @@
"message": "Set Up as Standalone Device",
"description": "Only available on development modes, menu option to open up the standalone device setup sequence"
},
"contextMenuCopyLink": {
"message": "Copy Link",
"description": "Shown in the context menu for a link to indicate that the user can copy the link"
},
"contextMenuNoSuggestions": {
"message": "No Suggestions",
"description": "Shown in the context menu for a misspelled word to indicate that there are no suggestions to replace the misspelled word"
},
"avatarMenuViewArchive": {
"message": "View Archive",
"description": "One of the menu options available in the Avatar Popup menu"
@ -1080,6 +1088,10 @@
"message": "Enable spell check of text entered in message composition box",
"description": "Description of the media permission description"
},
"spellCheckDirty": {
"message": "You must restart Signal to apply your new settings",
"description": "Shown when the user changes their spellcheck setting to indicate that they must restart Signal."
},
"clearDataHeader": {
"message": "Clear Data",
"description": "Header in the settings dialog for the section dealing with data deletion"

102
app/spell_check.js Normal file
View file

@ -0,0 +1,102 @@
/* global exports, require */
/* eslint-disable strict */
const { Menu, clipboard } = require('electron');
const osLocale = require('os-locale');
exports.setup = (browserWindow, messages) => {
const { session } = browserWindow.webContents;
const userLocale = osLocale.sync().replace(/_/g, '-');
const userLocales = [userLocale, userLocale.split('-')[0]];
const available = session.availableSpellCheckerLanguages;
const languages = userLocales.filter(l => available.includes(l));
console.log(`spellcheck: user locale: ${userLocale}`);
console.log('spellcheck: available spellchecker languages: ', available);
console.log('spellcheck: setting languages to: ', languages);
session.setSpellCheckerLanguages(languages);
browserWindow.webContents.on('context-menu', (_event, params) => {
const { editFlags } = params;
const isMisspelled = Boolean(params.misspelledWord);
const isLink = Boolean(params.linkURL);
const showMenu = params.isEditable || editFlags.canCopy || isLink;
// Popup editor menu
if (showMenu) {
const template = [];
if (isMisspelled) {
if (params.dictionarySuggestions.length > 0) {
template.push(
...params.dictionarySuggestions.map(label => ({
label,
click: () => {
browserWindow.webContents.replaceMisspelling(label);
},
}))
);
} else {
template.push({
label: messages.contextMenuNoSuggestions.message,
enabled: false,
});
}
template.push({ type: 'separator' });
}
if (params.isEditable) {
if (editFlags.canUndo) {
template.push({ label: messages.editMenuUndo.message, role: 'undo' });
}
// This is only ever `true` if undo was triggered via the context menu
// (not ctrl/cmd+z)
if (editFlags.canRedo) {
template.push({ label: messages.editMenuRedo.message, role: 'redo' });
}
if (editFlags.canUndo || editFlags.canRedo) {
template.push({ type: 'separator' });
}
if (editFlags.canCut) {
template.push({ label: messages.editMenuCut.message, role: 'cut' });
}
}
if (editFlags.canCopy || isLink) {
template.push({
label: isLink
? messages.contextMenuCopyLink.message
: messages.editMenuCopy.message,
role: isLink ? undefined : 'copy',
click: isLink
? () => {
clipboard.writeText(params.linkURL);
}
: undefined,
});
}
if (editFlags.canPaste) {
template.push({ label: messages.editMenuPaste.message, role: 'paste' });
}
if (editFlags.canPaste) {
template.push({
label: messages.editMenuPasteAndMatchStyle.message,
role: 'pasteAndMatchStyle',
});
}
// Only enable select all in editors because select all in non-editors
// results in all the UI being selected
if (editFlags.canSelectAll && params.isEditable) {
template.push({
label: messages.editMenuSelectAll.message,
role: 'selectall',
});
}
const menu = Menu.buildFromTemplate(template);
menu.popup(browserWindow);
}
});
};

View file

@ -334,7 +334,6 @@
getSpellCheck: () => storage.get('spell-check', true),
setSpellCheck: value => {
storage.put('spell-check', value);
startSpellCheck();
},
// eslint-disable-next-line eqeqeq
@ -545,19 +544,6 @@
}
});
const startSpellCheck = () => {
if (!window.enableSpellCheck || !window.disableSpellCheck) {
return;
}
if (window.Events.getSpellCheck()) {
window.enableSpellCheck();
} else {
window.disableSpellCheck();
}
};
startSpellCheck();
try {
await Promise.all([
ConversationController.load(),

View file

@ -1,172 +0,0 @@
/* global require, process, _ */
/* eslint-disable strict */
const electron = require('electron');
const Typo = require('typo-js');
const osLocale = require('os-locale');
const { remote, webFrame } = electron;
// `remote.require` since `Menu` is a main-process module.
const buildEditorContextMenu = remote.require('electron-editor-context-menu');
const EN_VARIANT = /^en/;
// Prevent the spellchecker from showing contractions as errors.
const ENGLISH_SKIP_WORDS = [
'ain',
'couldn',
'didn',
'doesn',
'hadn',
'hasn',
'mightn',
'mustn',
'needn',
'oughtn',
'shan',
'shouldn',
'wasn',
'weren',
'wouldn',
];
function setupLinux(locale) {
if (EN_VARIANT.test(locale)) {
window.log.info('Detected English locale on Linux. Enabling spell check.');
return new Typo(locale);
}
window.log.info(
'Detected non-English locale on Linux. Disabling spell check.'
);
return null;
}
// We load locale this way and not via app.getLocale() because this call returns
// 'es_ES' and not just 'es.' And hunspell requires the fully-qualified locale.
const locale = osLocale.sync().replace('-', '_');
// The LANG environment variable is how node spellchecker finds its default language:
// https://github.com/atom/node-spellchecker/blob/59d2d5eee5785c4b34e9669cd5d987181d17c098/lib/spellchecker.js#L29
if (!process.env.LANG) {
process.env.LANG = locale;
}
let spellchecker = null;
if (process.platform === 'linux') {
spellchecker = setupLinux(locale);
} else {
spellchecker = new Typo(locale);
// OSX and Windows 8+ have OS-level spellcheck APIs
window.log.info(
'Using OS-level spell check API with locale',
process.env.LANG
);
}
const simpleChecker = {
spellCheck(words, callback) {
const mispelled = words.filter(word => this.isMisspelled(word));
callback(mispelled);
},
isMisspelled(word) {
if (!spellchecker) {
return false;
}
const misspelled = !spellchecker.check(word);
// The idea is to make this as fast as possible. For the many, many calls which
// don't result in the red squiggly, we minimize the number of checks.
if (!misspelled) {
return false;
}
// Only if we think we've found an error do we check the locale and skip list.
if (locale.match(EN_VARIANT) && _.contains(ENGLISH_SKIP_WORDS, word)) {
return false;
}
return true;
},
getSuggestions(text) {
if (!spellchecker) {
return [];
}
return spellchecker.suggest(text);
},
add() {},
};
const dummyChecker = {
spellCheck(words, callback) {
callback([]);
},
isMisspelled() {
return false;
},
getSuggestions() {
return [];
},
add() {
// nothing
},
};
window.spellChecker = simpleChecker;
window.disableSpellCheck = () => {
window.removeEventListener('contextmenu', spellCheckHandler);
window.addEventListener('contextmenu', defaultContextMenuHandler);
webFrame.setSpellCheckProvider('en-US', dummyChecker);
};
window.enableSpellCheck = () => {
webFrame.setSpellCheckProvider('en-US', simpleChecker);
window.addEventListener('contextmenu', spellCheckHandler);
window.removeEventListener('contextmenu', defaultContextMenuHandler);
};
const defaultContextMenuHandler = e => {
// Only show the context menu in text editors.
if (!e.target.closest('textarea, input, [contenteditable="true"]')) {
return;
}
const menu = buildEditorContextMenu({});
// @see js/spell_check.js:177
setTimeout(() => {
menu.popup(remote.getCurrentWindow());
}, 30);
};
const spellCheckHandler = e => {
// Only show the context menu in text editors.
if (!e.target.closest('textarea, input, [contenteditable="true"]')) {
return;
}
const selectedText = window.getSelection().toString();
const isMisspelled = selectedText && simpleChecker.isMisspelled(selectedText);
const spellingSuggestions =
isMisspelled && simpleChecker.getSuggestions(selectedText).slice(0, 5);
const menu = buildEditorContextMenu({
isMisspelled,
spellingSuggestions,
});
// The 'contextmenu' event is emitted after 'selectionchange' has fired
// but possibly before the visible selection has changed. Try to wait
// to show the menu until after that, otherwise the visible selection
// will update after the menu dismisses and look weird.
setTimeout(() => {
menu.popup(remote.getCurrentWindow());
}, 30);
};

View file

@ -106,7 +106,17 @@
el: this.$('.spell-check-setting'),
name: 'spell-check-setting',
value: window.initialData.spellCheck,
setFn: window.setSpellCheck,
setFn: val => {
const $msg = this.$('.spell-check-setting-message');
if (val !== window.appStartInitialSpellcheckSetting) {
$msg.show();
$msg.attr('aria-hidden', false);
} else {
$msg.hide();
$msg.attr('aria-hidden', true);
}
window.setSpellCheck(val);
},
});
if (Settings.isHideMenuBarSupported()) {
new CheckboxView({
@ -131,6 +141,10 @@
'click .clear-data': 'onClearData',
},
render_attributes() {
const spellCheckDirty =
window.initialData.spellCheck !==
window.appStartInitialSpellcheckSetting;
return {
deviceNameLabel: i18n('deviceName'),
deviceName: window.initialData.deviceName,
@ -157,6 +171,9 @@
mediaPermissionsDescription: i18n('mediaPermissionsDescription'),
generalHeader: i18n('general'),
spellCheckDescription: i18n('spellCheckDescription'),
spellCheckHidden: spellCheckDirty ? 'false' : 'true',
spellCheckDisplay: spellCheckDirty ? 'inherit' : 'none',
spellCheckDirtyText: i18n('spellCheckDirty'),
};
},
onClose() {

25
main.js
View file

@ -16,6 +16,7 @@ const electron = require('electron');
const packageJson = require('./package.json');
const GlobalErrors = require('./app/global_errors');
const { setup: setupSpellChecker } = require('./app/spell_check');
GlobalErrors.addHandler();
@ -94,6 +95,19 @@ const {
} = require('./app/protocol_filter');
const { installPermissionsHandler } = require('./app/permissions');
let appStartInitialSpellcheckSetting = true;
async function getSpellCheckSetting() {
const json = await sql.getItemById('spell-check');
// Default to `true` if setting doesn't exist yet
if (!json) {
return true;
}
return json.value;
}
function showWindow() {
if (!mainWindow) {
return;
@ -182,6 +196,7 @@ function prepareURL(pathSegments, moreKeys) {
contentProxyUrl: config.contentProxyUrl,
importMode: importMode ? true : undefined, // for stringify()
serverTrustRoot: config.get('serverTrustRoot'),
appStartInitialSpellcheckSetting,
...moreKeys,
},
});
@ -240,7 +255,7 @@ function isVisible(window, bounds) {
);
}
function createWindow() {
async function createWindow() {
const { screen } = electron;
const windowOptions = Object.assign(
{
@ -260,6 +275,7 @@ function createWindow() {
contextIsolation: false,
preload: path.join(__dirname, 'preload.js'),
nativeWindowOpen: true,
spellcheck: await getSpellCheckSetting(),
},
icon: path.join(__dirname, 'images', 'icon_256.png'),
},
@ -296,6 +312,7 @@ function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow(windowOptions);
setupSpellChecker(mainWindow, locale.messages);
if (!usingTrayIcon && windowConfig && windowConfig.maximized) {
mainWindow.maximize();
}
@ -525,7 +542,7 @@ function showAbout() {
}
let settingsWindow;
async function showSettingsWindow() {
function showSettingsWindow() {
if (settingsWindow) {
settingsWindow.show();
return;
@ -621,10 +638,12 @@ async function showStickerCreator() {
contextIsolation: false,
preload: path.join(__dirname, 'sticker-creator/preload.js'),
nativeWindowOpen: true,
spellcheck: await getSpellCheckSetting(),
},
};
stickerCreatorWindow = new BrowserWindow(options);
setupSpellChecker(stickerCreatorWindow, locale.messages);
handleCommonWindowEvents(stickerCreatorWindow);
@ -797,6 +816,8 @@ app.on('ready', async () => {
console.log('sql.initialize was unsuccessful; returning early');
return;
}
// eslint-disable-next-line more/no-then
appStartInitialSpellcheckSetting = await getSpellCheckSetting();
await sqlChannels.initialize();
try {

View file

@ -75,8 +75,6 @@
"copy-text-to-clipboard": "2.1.0",
"curve25519-n": "https://github.com/scottnonnenberg-signal/node-curve25519.git#3e94f60bc54b2426476520d8d1a0aa835c25f5cc",
"draft-js": "0.10.5",
"electron-context-menu": "0.11.0",
"electron-editor-context-menu": "1.1.1",
"electron-mocha": "8.1.1",
"electron-notarize": "0.1.1",
"emoji-datasource": "5.0.1",
@ -139,7 +137,6 @@
"tmp": "0.0.33",
"to-arraybuffer": "1.0.1",
"typeface-inter": "3.10.0",
"typo-js": "1.1.0",
"underscore": "1.9.0",
"uuid": "3.3.2",
"websocket": "1.0.28"

View file

@ -1,21 +0,0 @@
diff --git a/node_modules/typo-js/typo.js b/node_modules/typo-js/typo.js
index 68c285b..95ebc6c 100644
--- a/node_modules/typo-js/typo.js
+++ b/node_modules/typo-js/typo.js
@@ -431,7 +431,7 @@ Typo.prototype = {
dictionaryTable[word] = null;
}
- if (rules.length > 0) {
+ if (rules && rules.length > 0) {
if (dictionaryTable[word] === null) {
dictionaryTable[word] = [];
}
@@ -546,6 +546,7 @@ Typo.prototype = {
else if (this.flags.FLAG === "num") {
return textCodes.split(",");
}
+ return [];
},
/**

View file

@ -395,23 +395,17 @@ try {
window.Signal.Debug = require('./js/modules/debug');
window.Signal.Logs = require('./js/modules/logs');
// Add right-click listener for selected text and urls
const contextMenu = require('electron-context-menu');
contextMenu({
showInspectElement: false,
shouldShowMenu: (event, params) =>
Boolean(
!params.isEditable &&
params.mediaType === 'none' &&
(params.linkURL || params.selectionText)
),
window.addEventListener('contextmenu', e => {
const editable = e.target.closest(
'textarea, input, [contenteditable="true"]'
);
const link = e.target.closest('a');
const selection = Boolean(window.getSelection().toString());
if (!editable && !selection && !link) {
e.preventDefault();
}
});
// We pull this in last, because the native module involved appears to be sensitive to
// /tmp mounted as noexec on Linux.
require('./js/spell_check');
if (config.environment === 'test') {
/* eslint-disable global-require, import/no-extraneous-dependencies */
window.test = {

View file

@ -100,6 +100,9 @@
<div class='spell-check-setting'>
<input type='checkbox' name='spell-check-setting' id='spell-check-setting' />
<label for='spell-check-setting'>{{ spellCheckDescription }}</label>
<p class='spell-check-setting-message' style='display: {{ spellCheckDisplay }};' aria-hidden='{{ spellCheckHidden }}'>
{{ spellCheckDirtyText }}
</p>
</div>
<hr>
<div class='permissions-setting'>

View file

@ -14,6 +14,8 @@ const { nativeTheme } = remote.require('electron');
window.platform = process.platform;
window.theme = config.theme;
window.i18n = i18n.setup(locale, localeMessages);
window.appStartInitialSpellcheckSetting =
config.appStartInitialSpellcheckSetting === 'true';
function setSystemTheme() {
window.systemTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';

View file

@ -407,7 +407,6 @@
<script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="database_test.js"></script>
<script type="text/javascript" src="i18n_test.js"></script>
<script type="text/javascript" src="spellcheck_test.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->

View file

@ -1,15 +0,0 @@
describe('spellChecker', () => {
it('should work', () => {
let result = null;
window.spellChecker.spellCheck(['correct'], answer => {
result = answer;
});
assert.deepEqual(result, []);
window.spellChecker.spellCheck(['fhqwgads'], answer => {
result = answer;
});
assert.deepEqual(result, ['fhqwgads']);
});
});

View file

@ -883,73 +883,81 @@
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " el: this.$('.menu-bar-setting'),",
"lineNumber": 113,
"line": " const $msg = this.$('.spell-check-setting-message');",
"lineNumber": 110,
"reasonCategory": "usageTrusted",
"updated": "2019-04-08T18:24:35.255Z",
"updated": "2020-03-19T16:06:32.598Z"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " el: this.$('.menu-bar-setting'),",
"lineNumber": 123,
"reasonCategory": "usageTrusted",
"updated": "2020-03-20T16:47:14.450Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " el: this.$('.media-permissions'),",
"lineNumber": 120,
"lineNumber": 130,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2020-03-20T16:47:14.450Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " this.$('.sync-setting').append(syncView.el);",
"lineNumber": 126,
"lineNumber": 136,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2020-03-20T16:47:14.450Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/settings_view.js",
"line": " this.$('.sync-setting').append(syncView.el);",
"lineNumber": 126,
"lineNumber": 136,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"updated": "2020-03-20T16:47:14.450Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " this.$('.sync').text(i18n('syncNow'));",
"lineNumber": 181,
"lineNumber": 198,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2020-03-20T19:55:50.800Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " this.$('.sync').attr('disabled', 'disabled');",
"lineNumber": 185,
"lineNumber": 202,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2020-03-20T19:55:50.800Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " this.$('.synced_at').hide();",
"lineNumber": 197,
"lineNumber": 214,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2020-03-20T19:55:50.800Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/settings_view.js",
"line": " this.$('.sync_failed').hide();",
"lineNumber": 202,
"lineNumber": 219,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"updated": "2020-03-20T19:55:50.800Z",
"reasonDetail": "Protected from arbitrary input"
},
{
@ -3141,22 +3149,6 @@
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z"
},
{
"rule": "jQuery-prepend(",
"path": "node_modules/electron-context-menu/index.js",
"line": "\t\t\tconst result = options.prepend(props, win);",
"lineNumber": 122,
"reasonCategory": "falseMatch",
"updated": "2019-04-10T19:08:25.356Z"
},
{
"rule": "jQuery-append(",
"path": "node_modules/electron-context-menu/index.js",
"line": "\t\t\tconst result = options.append(props, win);",
"lineNumber": 130,
"reasonCategory": "falseMatch",
"updated": "2019-04-10T19:08:25.356Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/electron-download/node_modules/debug/src/browser.js",
@ -11816,4 +11808,4 @@
"reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z"
}
]
]

103
yarn.lock
View file

@ -6181,23 +6181,6 @@ electron-chromedriver@~3.0.0:
electron-download "^4.1.0"
extract-zip "^1.6.5"
electron-context-menu@0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-0.11.0.tgz#3ecefb0231151add474c9b0df2fb66fde3ad5731"
integrity sha512-sgDIGqjgazUQ5fbfz0ObRkmODAsw00eylQprp5q4jyuL6dskd27yslhoJTrjLbFGErfVVYzRXPW2rQPJxARKmg==
dependencies:
electron-dl "^1.2.0"
electron-is-dev "^1.0.1"
electron-dl@^1.2.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-1.14.0.tgz#1466f1b945664ca3d784268307c2b935728177bf"
integrity sha512-4okyei42a1mLsvLK7hLrIfd20EQzB18nIlLTwBV992aMSmTGLUEFRTmO1MfSslGNrzD8nuPuy1l/VxO8so4lig==
dependencies:
ext-name "^5.0.0"
pupa "^1.0.0"
unused-filename "^1.0.0"
electron-download@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.0.tgz#bf932c746f2f87ffcc09d1dd472f2ff6b9187845"
@ -6212,17 +6195,6 @@ electron-download@^4.1.0:
semver "^5.3.0"
sumchecker "^2.0.1"
electron-editor-context-menu@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/electron-editor-context-menu/-/electron-editor-context-menu-1.1.1.tgz#dc30098e0dfb37f62628e43303124c7f3379572d"
integrity sha1-3DAJjg37N/YmKOQzAxJMfzN5Vy0=
dependencies:
lodash.clonedeep "^4.3.0"
lodash.defaults "^4.0.1"
lodash.isarray "^4.0.0"
lodash.isempty "^4.1.2"
lodash.isfunction "^3.0.8"
electron-icon-maker@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/electron-icon-maker/-/electron-icon-maker-0.0.3.tgz#bcd2e91896d7200f84fcc6652aed924fdaaa8307"
@ -6232,11 +6204,6 @@ electron-icon-maker@0.0.3:
icon-gen "^1.0.7"
jimp "^0.2.27"
electron-is-dev@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.0.1.tgz#6e0a184736fe7aea77d18210b0b0f6a02402c4bc"
integrity sha512-iwM3EotA9HTXqMGpQRkR/kT8OZqBbdfHTnlwcxsjSLYqY8svvsq0MuujsWCn3/vtgRmDv/PC/gKUUpoZvi5C1w==
electron-mocha@8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-8.1.1.tgz#e540e7d9ba80a024007a18533ae491c18f9a0ce2"
@ -6950,21 +6917,6 @@ express@^4.17.0, express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
ext-list@^2.0.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==
dependencies:
mime-db "^1.28.0"
ext-name@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6"
integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==
dependencies:
ext-list "^2.0.0"
sort-keys-length "^1.0.0"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@ -10264,10 +10216,6 @@ lodash.camelcase@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.clonedeep@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
lodash.cond@^4.3.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
@ -10277,26 +10225,10 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.defaults@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
lodash.isarray@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403"
lodash.isempty@^4.1.2:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
lodash.isfunction@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.8.tgz#4db709fc81bc4a8fd7127a458a5346c5cdce2c6b"
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -10804,11 +10736,6 @@ mime-db@1.40.0, "mime-db@>= 1.40.0 < 2":
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
mime-db@^1.28.0:
version "1.39.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.39.0.tgz#f95a20275742f7d2ad0429acfe40f4233543780e"
integrity sha512-DTsrw/iWVvwHH+9Otxccdyy0Tgiil6TWK/xhfARJZF/QFhwOgZgOIvA2/VIGpM8U7Q8z5nDmdDWC6tuVMJNibw==
mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
@ -11104,11 +11031,6 @@ mocha@~6.2.0:
yargs-parser "13.0.0"
yargs-unparser "1.5.0"
modify-filename@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1"
integrity sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=
moment@2.21.0:
version "2.21.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a"
@ -13156,11 +13078,6 @@ punycode@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
pupa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pupa/-/pupa-1.0.0.tgz#9a9568a5af7e657b8462a6e9d5328743560ceff6"
integrity sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y=
pupa@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726"
@ -15312,13 +15229,6 @@ socks@~2.2.0:
ip "^1.1.5"
smart-buffer "^4.0.1"
sort-keys-length@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188"
integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=
dependencies:
sort-keys "^1.0.0"
sort-keys@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
@ -16622,11 +16532,6 @@ typescript@3.7.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19"
integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==
typo-js@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.1.0.tgz#a5a9f592bcb453666bf70c9694da58705d025ed8"
integrity sha512-W3kLbx+ML9PBl5Bzso/lTvVxk4BCveSNAtQeht59FEtxCdGThmn6wSHA4Xq3eQYAK24NHdisMM4JmsK0GFy/pg==
ua-parser-js@^0.7.18:
version "0.7.19"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
@ -16855,14 +16760,6 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
unused-filename@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-1.0.0.tgz#d340880f71ae2115ebaa1325bef05cc6684469c6"
integrity sha1-00CID3GuIRXrqhMlvvBcxmhEacY=
dependencies:
modify-filename "^1.1.0"
path-exists "^3.0.0"
unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"