$ date
--- stdout ---
Mon Mar 31 08:22:44 UTC 2025
--- end ---
$ git clone file:///srv/git/mediawiki-extensions-Wikispeech.git repo --depth=1 -b master
--- stderr ---
Cloning into 'repo'...
--- stdout ---
--- end ---
$ git config user.name libraryupgrader
--- stdout ---
--- end ---
$ git config user.email tools.libraryupgrader@tools.wmflabs.org
--- stdout ---
--- end ---
$ git submodule update --init
--- stdout ---
--- end ---
$ grr init
--- stdout ---
Installed commit-msg hook.
--- end ---
$ git show-ref refs/heads/master
--- stdout ---
112175da7e6323f043e1699ea5f73307b3fd9cb5 refs/heads/master
--- end ---
$ /usr/bin/npm audit --json
--- stdout ---
{
"auditReportVersion": 2,
"vulnerabilities": {},
"metadata": {
"vulnerabilities": {
"info": 0,
"low": 0,
"moderate": 0,
"high": 0,
"critical": 0,
"total": 0
},
"dependencies": {
"prod": 1,
"dev": 442,
"optional": 0,
"peer": 1,
"peerOptional": 0,
"total": 442
}
}
}
--- end ---
$ /usr/bin/composer install
--- stderr ---
No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.
Loading composer repositories with package information
Updating dependencies
Lock file operations: 38 installs, 0 updates, 0 removals
- Locking composer/pcre (3.3.2)
- Locking composer/semver (3.4.3)
- Locking composer/spdx-licenses (1.5.8)
- Locking composer/xdebug-handler (3.0.5)
- Locking dealerdirect/phpcodesniffer-composer-installer (v1.0.0)
- Locking doctrine/deprecations (1.1.4)
- Locking felixfbecker/advanced-json-rpc (v3.2.1)
- Locking mediawiki/mediawiki-codesniffer (v46.0.0)
- Locking mediawiki/mediawiki-phan-config (0.15.1)
- Locking mediawiki/minus-x (1.1.3)
- Locking mediawiki/phan-taint-check-plugin (6.1.0)
- Locking microsoft/tolerant-php-parser (v0.1.2)
- Locking netresearch/jsonmapper (v4.5.0)
- Locking phan/phan (5.4.5)
- Locking php-parallel-lint/php-console-color (v1.0.1)
- Locking php-parallel-lint/php-console-highlighter (v1.0.0)
- Locking php-parallel-lint/php-parallel-lint (v1.4.0)
- Locking phpcsstandards/phpcsextra (1.2.1)
- Locking phpcsstandards/phpcsutils (1.0.12)
- Locking phpdocumentor/reflection-common (2.2.0)
- Locking phpdocumentor/reflection-docblock (5.6.1)
- Locking phpdocumentor/type-resolver (1.10.0)
- Locking phpstan/phpdoc-parser (2.1.0)
- Locking psr/container (2.0.2)
- Locking psr/log (3.0.2)
- Locking sabre/event (5.1.7)
- Locking squizlabs/php_codesniffer (3.11.3)
- Locking symfony/console (v7.2.5)
- Locking symfony/deprecation-contracts (v3.5.1)
- Locking symfony/polyfill-ctype (v1.31.0)
- Locking symfony/polyfill-intl-grapheme (v1.31.0)
- Locking symfony/polyfill-intl-normalizer (v1.31.0)
- Locking symfony/polyfill-mbstring (v1.31.0)
- Locking symfony/polyfill-php80 (v1.31.0)
- Locking symfony/service-contracts (v3.5.1)
- Locking symfony/string (v7.2.0)
- Locking tysonandre/var_representation_polyfill (0.1.3)
- Locking webmozart/assert (1.11.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 38 installs, 0 updates, 0 removals
0 [>---------------------------] 0 [->--------------------------]
- Installing squizlabs/php_codesniffer (3.11.3): Extracting archive
- Installing dealerdirect/phpcodesniffer-composer-installer (v1.0.0): Extracting archive
- Installing composer/pcre (3.3.2): Extracting archive
- Installing symfony/polyfill-php80 (v1.31.0): Extracting archive
- Installing phpcsstandards/phpcsutils (1.0.12): Extracting archive
- Installing phpcsstandards/phpcsextra (1.2.1): Extracting archive
- Installing symfony/polyfill-mbstring (v1.31.0): Extracting archive
- Installing composer/spdx-licenses (1.5.8): Extracting archive
- Installing composer/semver (3.4.3): Extracting archive
- Installing mediawiki/mediawiki-codesniffer (v46.0.0): Extracting archive
- Installing tysonandre/var_representation_polyfill (0.1.3): Extracting archive
- Installing symfony/polyfill-intl-normalizer (v1.31.0): Extracting archive
- Installing symfony/polyfill-intl-grapheme (v1.31.0): Extracting archive
- Installing symfony/polyfill-ctype (v1.31.0): Extracting archive
- Installing symfony/string (v7.2.0): Extracting archive
- Installing symfony/deprecation-contracts (v3.5.1): Extracting archive
- Installing psr/container (2.0.2): Extracting archive
- Installing symfony/service-contracts (v3.5.1): Extracting archive
- Installing symfony/console (v7.2.5): Extracting archive
- Installing sabre/event (5.1.7): Extracting archive
- Installing netresearch/jsonmapper (v4.5.0): Extracting archive
- Installing microsoft/tolerant-php-parser (v0.1.2): Extracting archive
- Installing webmozart/assert (1.11.0): Extracting archive
- Installing phpstan/phpdoc-parser (2.1.0): Extracting archive
- Installing phpdocumentor/reflection-common (2.2.0): Extracting archive
- Installing doctrine/deprecations (1.1.4): Extracting archive
- Installing phpdocumentor/type-resolver (1.10.0): Extracting archive
- Installing phpdocumentor/reflection-docblock (5.6.1): Extracting archive
- Installing felixfbecker/advanced-json-rpc (v3.2.1): Extracting archive
- Installing psr/log (3.0.2): Extracting archive
- Installing composer/xdebug-handler (3.0.5): Extracting archive
- Installing phan/phan (5.4.5): Extracting archive
- Installing mediawiki/phan-taint-check-plugin (6.1.0): Extracting archive
- Installing mediawiki/mediawiki-phan-config (0.15.1): Extracting archive
- Installing mediawiki/minus-x (1.1.3): Extracting archive
- Installing php-parallel-lint/php-console-color (v1.0.1): Extracting archive
- Installing php-parallel-lint/php-console-highlighter (v1.0.0): Extracting archive
- Installing php-parallel-lint/php-parallel-lint (v1.4.0): Extracting archive
0/36 [>---------------------------] 0%
20/36 [===============>------------] 55%
35/36 [===========================>] 97%
36/36 [============================] 100%
1 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
16 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
--- stdout ---
PHP CodeSniffer Config installed_paths set to ../../mediawiki/mediawiki-codesniffer,../../phpcsstandards/phpcsextra,../../phpcsstandards/phpcsutils
--- end ---
Upgrading n:eslint-config-wikimedia from 0.28.2 -> 0.29.1
$ /usr/bin/npm install
--- stderr ---
npm WARN deprecated @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead
npm WARN deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
--- stdout ---
added 442 packages, and audited 443 packages in 5s
92 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json
--- end ---
$ /usr/bin/npm install grunt-eslint@24.3.0 --save-exact
--- stdout ---
up to date, audited 443 packages in 941ms
92 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json
--- end ---
$ ./node_modules/.bin/eslint . --fix
--- stdout ---
/src/repo/dev/speechoid-docker-compose/docker-compose.yml
18:1 warning This line has a length of 116. Maximum allowed is 100 max-len
23:1 warning This line has a length of 117. Maximum allowed is 100 max-len
29:1 warning This line has a length of 118. Maximum allowed is 100 max-len
48:1 warning This line has a length of 119. Maximum allowed is 100 max-len
55:1 warning This line has a length of 117. Maximum allowed is 100 max-len
69:1 warning This line has a length of 107. Maximum allowed is 100 max-len
78:1 warning This line has a length of 127. Maximum allowed is 100 max-len
/src/repo/modules/ext.wikispeech.gadget.js
39:3 error Prefer .then to .done no-jquery/no-done-fail
39:3 error Prefer .then to .fail no-jquery/no-done-fail
83:2 error Prefer .then to .done no-jquery/no-done-fail
107:1 warning The type 'ext' is undefined jsdoc/no-undefined-types
115:2 error Prefer .then to .done no-jquery/no-done-fail
115:2 error Prefer .then to .fail no-jquery/no-done-fail
144:3 error Prefer .then to .done no-jquery/no-done-fail
157:3 error Prefer .then to .done no-jquery/no-done-fail
174:1 error Prefer .then to .done no-jquery/no-done-fail
193:2 error Prefer .then to .done no-jquery/no-done-fail
204:3 error Prefer .then to .done no-jquery/no-done-fail
204:3 error Prefer .then to .fail no-jquery/no-done-fail
206:5 error Prefer .then to .done no-jquery/no-done-fail
206:5 error Prefer .then to .fail no-jquery/no-done-fail
208:7 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.highlighter.js
30:1 warning This line has a length of 108. Maximum allowed is 100 max-len
/src/repo/modules/ext.wikispeech.loader.js
10:2 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.main.js
83:1 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.player.js
54:3 error Prefer .then to .done no-jquery/no-done-fail
99:3 error Prefer .then to .done no-jquery/no-done-fail
99:3 error Prefer .then to .fail no-jquery/no-done-fail
112:5 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.selectionPlayer.js
144:3 error Prefer .then to .done no-jquery/no-done-fail
179:3 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.storage.js
41:3 error Prefer .then to .done no-jquery/no-done-fail
153:10 error Prefer .then to .done no-jquery/no-done-fail
197:19 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.transcriptionPreviewer.js
64:18 error Prefer .then to .done no-jquery/no-done-fail
64:18 error Prefer .then to .fail no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.ui.js
94:3 error Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.userOptionsDialog.js
95:2 error Prefer .then to .done no-jquery/no-done-fail
✖ 38 problems (29 errors, 9 warnings)
--- end ---
$ ./node_modules/.bin/eslint . -f json
--- stdout ---
[{"filePath":"/src/repo/.eslintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/.stylelintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/Gruntfile.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/composer.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/dev/speechoid-docker-compose/docker-compose.yml","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 116. Maximum allowed is 100.","line":18,"column":1,"nodeType":"Program","messageId":"max","endLine":18,"endColumn":117},{"ruleId":"max-len","severity":1,"message":"This line has a length of 117. Maximum allowed is 100.","line":23,"column":1,"nodeType":"Program","messageId":"max","endLine":23,"endColumn":118},{"ruleId":"max-len","severity":1,"message":"This line has a length of 118. Maximum allowed is 100.","line":29,"column":1,"nodeType":"Program","messageId":"max","endLine":29,"endColumn":119},{"ruleId":"max-len","severity":1,"message":"This line has a length of 119. Maximum allowed is 100.","line":48,"column":1,"nodeType":"Program","messageId":"max","endLine":48,"endColumn":120},{"ruleId":"max-len","severity":1,"message":"This line has a length of 117. Maximum allowed is 100.","line":55,"column":1,"nodeType":"Program","messageId":"max","endLine":55,"endColumn":118},{"ruleId":"max-len","severity":1,"message":"This line has a length of 107. Maximum allowed is 100.","line":69,"column":1,"nodeType":"Program","messageId":"max","endLine":69,"endColumn":108},{"ruleId":"max-len","severity":1,"message":"This line has a length of 127. Maximum allowed is 100.","line":78,"column":1,"nodeType":"Program","messageId":"max","endLine":78,"endColumn":128}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":7,"fixableErrorCount":0,"fixableWarningCount":0,"source":"# If you want to use non-default values in your config it's best to use an\n# override file rather than to edit this one. See\n# https://docs.docker.com/compose/multiple-compose-files/merge/ for details.\n\nversion: \"3.8\"\nname: speechoid\nservices:\n mariadb:\n image: mariadb:10.5.3\n restart: always\n environment:\n - MYSQL_ROOT_PASSWORD=root\n - MYSQL_DATABASE=pronlex\n - MYSQL_USER=pronlex\n - MYSQL_PASSWORD=pronlex\n\n ahotts:\n image: docker-registry.wikimedia.org/wikimedia/mediawiki-services-wikispeech-ahotts:2021-07-07-080449-production\n expose:\n - \"1200\"\n\n mishkal:\n image: docker-registry.wikimedia.org/wikimedia/mediawiki-services-wikispeech-mishkal:2025-01-21-125334-production\n expose:\n - \"8080\"\n entrypoint: ./interfaces/web/mishkal-webserver.py\n\n mary-tts:\n image: docker-registry.wikimedia.org/wikimedia/mediawiki-services-wikispeech-mary-tts:2024-02-28-153902-production\n environment:\n - MARY_TTS_MISHKAL_URL=http://mishkal:8080/\n - HAPROXY_MARY_TTS_BACKEND_MAXIMUM_CONCURRENT_CONNECTIONS=4\n # - HAPROXY_QUEUE_SIZE=100\n # - HAPROXY_TIMEOUT_CONNECT=60s\n # - HAPROXY_TIMEOUT_CLIENT=60s\n # - HAPROXY_TIMEOUT_SERVER=60s\n # - HAPROXY_MARY_TTS_FRONTEND_PORT=8080\n # - HAPROXY_MARY_TTS_BACKEND_PORT=59125\n # - HAPROXY_STATS_FRONTEND_REFRESH_RATE=4s\n expose:\n - \"8080\"\n volumes:\n - ./compose-files/wait-for-it.sh:/srv/compose/wait-for-it.sh\n - ./compose-files/mary-entrypoint.sh:/srv/compose/entrypoint.sh\n entrypoint: /srv/compose/entrypoint.sh\n\n symbolset:\n image: docker-registry.wikimedia.org/wikimedia/mediawiki-services-wikispeech-symbolset:2024-02-28-153900-production\n expose:\n - \"8771\"\n ports:\n - 8771:8771\n\n pronlex:\n image: docker-registry.wikimedia.org/wikimedia/mediawiki-services-wikispeech-pronlex:2024-02-28-153924-production\n expose:\n - \"8787\"\n # environment:\n # If this is set, Pronlex will connect to this MariaDB database.\n # If this NOT is set, Pronlex will use built in SQLite database.\n # - PRONLEX_MARIADB_URI=speechoid:password@tcp(wikispeech-tts-pronlex:3306)\n volumes:\n - ./compose-files/wait-for-it.sh:/srv/compose/wait-for-it.sh\n - ./compose-files/pronlex-entrypoint.sh:/srv/compose/entrypoint.sh\n entrypoint: /srv/compose/entrypoint.sh\n\n # This is a temporary workaround to handle low volume samples from MaryTTS.\n sox-proxy:\n image: docker-registry.wikimedia.org/repos/mediawiki/services/speechoid/sox-proxy:2024-03-18-production\n expose:\n - \"5000\"\n ports:\n - 5000:5000\n environment:\n - SPEECHOID_URL=http://wikispeech-server:10001/\n\n wikispeech-server:\n image: docker-registry.wikimedia.org/wikimedia/mediawiki-services-wikispeech-wikispeech-server:2024-02-28-153857-production\n expose:\n - \"10001\"\n ports:\n - 10000:10000\n - 10001:10001\n - 10002:10002\n volumes:\n - ./volumes/wikispeech-server_tmp:/srv/wikispeech-server/wikispeech_server/tmp\n - ./compose-files/wikispeech-server-entrypoint.sh:/srv/compose/entrypoint.sh\n - ./compose-files/wikispeech-server.conf:/srv/wikispeech-server/wikispeech_server/default.conf\n - ./compose-files/wait-for-it.sh:/srv/compose/wait-for-it.sh\n entrypoint: /srv/compose/entrypoint.sh\n environment:\n - HAPROXY_QUEUE_SIZE=100\n - HAPROXY_WIKISPEECH_SERVER_BACKEND_MAXIMUM_CONCURRENT_CONNECTIONS=1\n - HAPROXY_TIMEOUT_CONNECT=60s\n - HAPROXY_TIMEOUT_CLIENT=60s\n - HAPROXY_TIMEOUT_SERVER=60s\n - HAPROXY_WIKISPEECH_SERVER_BACKEND_PORT=10000\n - HAPROXY_WIKISPEECH_SERVER_FRONTEND_PORT=10001\n - HAPROXY_STATS_FRONTEND_PORT=10002\n - HAPROXY_STATS_FRONTEND_REFRESH_RATE=4s\n","usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-len","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/extension.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/i18n/api/en.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/i18n/api/qqq.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/i18n/en.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/i18n/qqq.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/jsdoc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/.eslintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.gadget.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":39,"column":3,"nodeType":"CallExpression","endLine":58,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .fail","line":39,"column":3,"nodeType":"CallExpression","endLine":65,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":83,"column":2,"nodeType":"CallExpression","endLine":97,"endColumn":5},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'ext' is undefined.","line":107,"column":1,"nodeType":"Block","endLine":107,"endColumn":1},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":115,"column":2,"nodeType":"CallExpression","endLine":123,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .fail","line":115,"column":2,"nodeType":"CallExpression","endLine":129,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":144,"column":3,"nodeType":"CallExpression","endLine":150,"endColumn":4},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":157,"column":3,"nodeType":"CallExpression","endLine":167,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":174,"column":1,"nodeType":"CallExpression","endLine":220,"endColumn":4},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":193,"column":2,"nodeType":"CallExpression","endLine":219,"endColumn":5},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":204,"column":3,"nodeType":"CallExpression","endLine":215,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .fail","line":204,"column":3,"nodeType":"CallExpression","endLine":218,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":206,"column":5,"nodeType":"CallExpression","endLine":211,"endColumn":9},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .fail","line":206,"column":5,"nodeType":"CallExpression","endLine":214,"endColumn":9},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":208,"column":7,"nodeType":"CallExpression","endLine":210,"endColumn":10}],"suppressedMessages":[],"errorCount":14,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Loads wikispeech modules from producer\n *\n * @module ext.wikispeech.gadget\n */\n\nlet moduleUrl, api, optionsPage;\n\n/**\n * Add config variables from the producer's config.\n *\n * The config values are specified in extension.json under\n * ResourceModules -> ext.wikispeech.gadget.\n */\nfunction addConfig() {\n\tconst config = require( './config.json' );\n\tObject.keys( config ).forEach( ( key ) => {\n\t\tconst value = config[ key ];\n\t\tmw.config.set( 'wg' + key, value );\n\t} );\n}\n\n/**\n * Read user options from the consumer wiki.\n *\n * User options are stored in a subpage to the user page called\n * \"Wikispeech_preferences\".\n *\n * @return {jQuery.Deferred} Resolves with an object containing the\n * user options. Resolves with the empty object if the options\n * could not be read.\n */\nfunction getUserOptionsOnConsumer() {\n\tconst done = $.Deferred();\n\tif ( mw.user.isAnon() ) {\n\t\t// No user options set if not logged in.\n\t\tdone.resolve( {} );\n\t} else {\n\t\tapi.get( {\n\t\t\taction: 'parse',\n\t\t\tpage: optionsPage,\n\t\t\tprop: 'wikitext',\n\t\t\tformatversion: 2\n\t\t} )\n\t\t\t.done( ( response ) => {\n\t\t\t\tconst content = response.parse.wikitext;\n\t\t\t\tlet options;\n\t\t\t\ttry {\n\t\t\t\t\toptions = JSON.parse( content );\n\t\t\t\t} catch ( error ) {\n\t\t\t\t\tmw.log.warn(\n\t\t\t\t\t\t'[Wikispeech] Failed to parse user preferences, ' +\n\t\t\t\t\t\t\t'using defaults: ' + error\n\t\t\t\t\t);\n\t\t\t\t\toptions = {};\n\t\t\t\t}\n\t\t\t\tdone.resolve( options );\n\t\t\t} )\n\t\t\t.fail( ( error ) => {\n\t\t\t\tmw.log.warn(\n\t\t\t\t\t'[Wikispeech] Failed to load user preferences page, ' +\n\t\t\t\t\t\t'using defaults: ' + error\n\t\t\t\t);\n\t\t\t\tdone.resolve( {} );\n\t\t\t} );\n\t}\n\treturn done;\n}\n\n/**\n * Add user options for Wikispeech.\n *\n * Reads user options from the consumer wiki and add those. Any\n * option that is not present on the consumer wiki will be set to the\n * default from the producer wiki.\n *\n * @return {jQuery.Deferred} Resolves when user options have been\n * read.\n */\nfunction addUserOptions() {\n\tconst done = $.Deferred();\n\tconst defaultOptions = require( './default-user-options.json' );\n\tgetUserOptionsOnConsumer().done( ( options ) => {\n\t\tObject.keys( defaultOptions ).forEach( ( key ) => {\n\t\t\tlet value;\n\n\t\t\t// Take the option value from user page if it is set,\n\t\t\t// otherwise use default.\n\t\t\tif ( Object.keys( options ).includes( key ) ) {\n\t\t\t\tvalue = options[ key ];\n\t\t\t} else {\n\t\t\t\tvalue = defaultOptions[ key ];\n\t\t\t}\n\t\t\tmw.user.options.set( key, value );\n\t\t\tdone.resolve();\n\t\t} );\n\t} );\n\treturn done;\n}\n\n/**\n * Write user options to a subpage to the user page.\n *\n * User options are read from the preferences popup dialog and\n * stored as JSON.\n *\n * @param {ext.wikispeech.UserOptionsDialog} dialog\n */\nfunction writeUserOptionsToWikiPage( dialog ) {\n\tconst options = require( './default-user-options.json' );\n\tconst voice = dialog.getVoice();\n\toptions[ voice.variable ] = voice.voice;\n\toptions.wikispeechSpeechRate = dialog.getSpeechRate();\n\tconst optionsJson = JSON.stringify( options, null, 4 );\n\tapi.postWithEditToken( {\n\t\taction: 'edit',\n\t\ttitle: optionsPage,\n\t\ttext: optionsJson,\n\t\tformatversion: 2\n\t} )\n\t\t.done( () => {\n\t\t\tmw.log( '[Wikispeech] Wrote user preferences to \"' + optionsPage + '\".' );\n\t\t} )\n\t\t.fail( ( error ) => {\n\t\t\tmw.log.warn(\n\t\t\t\t'[Wikispeech] Failed to write user preferences to \"' +\n\t\t\t\t\toptionsPage + '\": ' + error\n\t\t\t);\n\t\t} );\n}\n\n/**\n * Add consumer specific elements to the UI.\n *\n * Adds a popup dialog for changing user options and a button on\n * the player toolbar to open it.\n */\nfunction extendUi() {\n\tconst UserOptionsDialog = require( './ext.wikispeech.userOptionsDialog.js' );\n\tconst dialog = new UserOptionsDialog();\n\tmw.wikispeech.ui.addWindow( dialog );\n\tconst gadgetGroup = mw.wikispeech.ui.addToolbarGroup();\n\tmw.wikispeech.ui.addButton( gadgetGroup, 'settings', () => {\n\t\tmw.wikispeech.ui.openWindow( dialog ).done(\n\t\t\t( data ) => {\n\t\t\t\tif ( data && data.action === 'save' ) {\n\t\t\t\t\twriteUserOptionsToWikiPage( dialog );\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t} );\n\tif ( mw.config.get( 'wgWikispeechAllowConsumerEdits' ) ) {\n\t\tconst producerApi = new mw.ForeignApi(\n\t\t\tmw.wikispeech.producerUrl +\n\t\t\t\t'/api.php'\n\t\t);\n\t\tproducerApi.get( {\n\t\t\taction: 'query',\n\t\t\tformat: 'json',\n\t\t\tmeta: 'siteinfo',\n\t\t\tsiprop: 'general'\n\t\t} )\n\t\t\t.done( ( response ) => {\n\t\t\t\tconst producerInfo = response.query.general,\n\t\t\t\t\tscriptPath = producerInfo.server + producerInfo.script;\n\t\t\t\tmw.wikispeech.ui.addEditButton( scriptPath );\n\t\t\t} );\n\t}\n}\n\nmw.wikispeech = mw.wikispeech || {};\nmw.wikispeech.consumerMode = true;\n\nmw.loader.using( [\n\t'mediawiki.api',\n\t'mediawiki.user',\n\t'mediawiki.ForeignApi',\n\t'oojs-ui',\n\t'oojs-ui-core',\n\t'oojs-ui-toolbars',\n\t'oojs-ui-windows',\n\t'oojs-ui.styles.icons-media',\n\t'oojs-ui.styles.icons-movement',\n\t'oojs-ui.styles.icons-interactions',\n\t'oojs-ui.styles.icons-editing-core'\n] ).done( () => {\n\taddConfig();\n\tconst namespace = mw.config.get( 'wgNamespaceIds' ).user;\n\tconst userPage = mw.Title.makeTitle( namespace, mw.user.getName() )\n\t\t.getPrefixedText();\n\toptionsPage = userPage + '/Wikispeech_preferences';\n\tapi = new mw.Api();\n\taddUserOptions().done( () => {\n\t\tconst parametersString = $.param( {\n\t\t\tlang: mw.config.get( 'wgUserLanguage' ),\n\t\t\tskin: mw.config.get( 'skin' ),\n\t\t\traw: 1,\n\t\t\tsafemode: 1,\n\t\t\tmodules: 'ext.wikispeech'\n\t\t} );\n\t\tmoduleUrl = mw.wikispeech.producerUrl + '/load.php?' +\n\t\t\tparametersString;\n\t\tmw.log( '[Wikispeech] Loading wikispeech module from ' + moduleUrl );\n\t\tmw.loader.getScript( moduleUrl )\n\t\t\t.done( () => {\n\t\t\t\tmw.loader.using( 'ext.wikispeech' )\n\t\t\t\t\t.done( () => {\n\t\t\t\t\t\tmw.wikispeech.ui.ready.done( () => {\n\t\t\t\t\t\t\textendUi();\n\t\t\t\t\t\t} );\n\t\t\t\t\t} )\n\t\t\t\t\t.fail( ( error ) => {\n\t\t\t\t\t\tmw.log.error( '[Wikispeech] Failed to load Wikispeech module: ' + error );\n\t\t\t\t\t} );\n\t\t\t} )\n\t\t\t.fail( ( error ) => {\n\t\t\t\tmw.log.error( '[Wikispeech] Failed to load Wikispeech module: ' + error );\n\t\t\t} );\n\t} );\n} );\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.highlighter.js","messages":[{"ruleId":"max-len","severity":1,"message":"This line has a length of 108. Maximum allowed is 100.","line":30,"column":1,"nodeType":"Program","messageId":"max","endLine":30,"endColumn":103}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":33,"column":16,"nodeType":"CallExpression","endLine":34,"endColumn":48,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Handles highlighting parts of the page when reciting.\n *\n * @class ext.wikispeech.Highlighter\n * @constructor\n */\n\nfunction Highlighter() {\n\tconst self = this;\n\tself.highlightTokenTimer = null;\n\tself.utteranceHighlightingClass =\n\t\t'ext-wikispeech-highlight-sentence';\n\tself.utteranceHighlightingSelector =\n\t\t'.' + self.utteranceHighlightingClass;\n\n\t/**\n\t * Highlight text associated with an utterance.\n\t *\n\t * Adds highlight spans to the text nodes from which the\n\t * tokens of the utterance were created. For first and last node,\n\t * it's possible that only part of the text is highlighted,\n\t * since they may contain start/end of next/previous\n\t * utterance.\n\t *\n\t * @param {Object} utterance The utterance to add\n\t * highlighting to.\n\t */\n\n\tthis.highlightUtterance = function ( utterance ) {\n\t\tconst textNodes = utterance.content.map( ( item ) => mw.wikispeech.storage.getNodeForItem( item ) );\n\t\t// Class name is documented above\n\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\tconst span = $( '<span>' )\n\t\t\t.addClass( self.utteranceHighlightingClass )\n\t\t\t.get( 0 );\n\t\tself.wrapTextNodes(\n\t\t\tspan,\n\t\t\ttextNodes,\n\t\t\tutterance.startOffset,\n\t\t\tutterance.endOffset\n\t\t);\n\t\t$( self.utteranceHighlightingSelector ).each( function ( i ) {\n\t\t\t// Save the path to the text node, as it was before\n\t\t\t// adding the span. This will no longer be the correct\n\t\t\t// path, once the span is added. This enables adding\n\t\t\t// token highlighting within the utterance\n\t\t\t// highlighting.\n\t\t\tthis.textPath = utterance.content[ i ].path;\n\t\t} );\n\t};\n\n\t/**\n\t * Wrap text nodes in an element.\n\t *\n\t * Each text node is wrapped in an individual copy of the\n\t * wrapper element. The first and last node will be partially\n\t * wrapped, based on the offset values.\n\t *\n\t * @param {HTMLElement} wrapper The element used to wrap the\n\t * text nodes.\n\t * @param {Text[]} textNodes The text nodes to wrap.\n\t * @param {number} startOffset The start offset in the first\n\t * text node.\n\t * @param {number} endOffset The end offset in the last text\n\t * node.\n\t */\n\n\tthis.wrapTextNodes = function (\n\t\twrapper,\n\t\ttextNodes,\n\t\tstartOffset,\n\t\tendOffset\n\t) {\n\t\tlet $nodesToWrap = $();\n\t\tconst firstNode = textNodes[ 0 ];\n\t\tif ( textNodes.length === 1 ) {\n\t\t\t// If there is only one node that should be wrapped,\n\t\t\t// split it twice; once for the start and once for the\n\t\t\t// end offset.\n\t\t\tfirstNode.splitText( startOffset );\n\t\t\tfirstNode.nextSibling.splitText( endOffset + 1 - startOffset );\n\t\t\t$nodesToWrap = $nodesToWrap.add( firstNode.nextSibling );\n\t\t} else {\n\t\t\tfirstNode.splitText( startOffset );\n\t\t\t// The first half of a split node remains as the\n\t\t\t// original node. Since we want the second half, we add\n\t\t\t// the following node.\n\t\t\t$nodesToWrap = $nodesToWrap.add( firstNode.nextSibling );\n\t\t\tfor ( let i = 1; i < textNodes.length - 1; i++ ) {\n\t\t\t\tconst node = textNodes[ i ];\n\t\t\t\t// Wrap all the nodes between first and last\n\t\t\t\t// completely.\n\t\t\t\t$nodesToWrap = $nodesToWrap.add( node );\n\t\t\t}\n\t\t\tconst lastNode = textNodes[ textNodes.length - 1 ];\n\t\t\tlastNode.splitText( endOffset + 1 );\n\t\t\t$nodesToWrap = $nodesToWrap.add( lastNode );\n\t\t}\n\t\t$nodesToWrap.wrap( wrapper );\n\t};\n\n\t/**\n\t * Highlight a token in the original HTML.\n\t *\n\t * What part of the HTML to wrap is calculated from a token.\n\t *\n\t * @param {Object} token The token used to calculate what part\n\t * to highlight.\n\t */\n\n\tthis.startTokenHighlighting = function ( token ) {\n\t\tself.removeWrappers( '.ext-wikispeech-highlight-word' );\n\t\tself.clearHighlightTokenTimer();\n\t\tself.highlightToken( token );\n\t\tself.setHighlightTokenTimer( token );\n\t};\n\n\t/**\n\t * Highlight a token in the original HTML.\n\t *\n\t * What part of the HTML to wrap is calculated from a token.\n\t *\n\t * @param {Object} token The token used to calculate what part\n\t * to highlight.\n\t */\n\n\tthis.highlightToken = function ( token ) {\n\t\tconst span = $( '<span>' )\n\t\t\t.addClass( 'ext-wikispeech-highlight-word' )\n\t\t\t.get( 0 );\n\t\tconst textNodes = token.items.map( ( item ) => {\n\t\t\tlet textNode;\n\n\t\t\tif ( $( self.utteranceHighlightingSelector ).length ) {\n\t\t\t\t// Add the token highlighting within the\n\t\t\t\t// utterance highlightings, if there are any.\n\t\t\t\ttextNode = self.getNodeInUtteranceHighlighting(\n\t\t\t\t\titem\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\ttextNode = mw.wikispeech.storage.getNodeForItem( item );\n\t\t\t}\n\t\t\treturn textNode;\n\t\t} );\n\t\tlet startOffset = token.startOffset;\n\t\tlet endOffset = token.endOffset;\n\t\tif (\n\t\t\t$( self.utteranceHighlightingSelector ).length &&\n\t\t\t\ttoken.items[ 0 ] === token.utterance.content[ 0 ]\n\t\t) {\n\t\t\t// Modify the offset if the token is the first in the\n\t\t\t// utterance and there is an utterance\n\t\t\t// highlighting. The text node may have been split\n\t\t\t// when the utterance highlighting was applied.\n\t\t\tstartOffset -= token.utterance.startOffset;\n\t\t\tendOffset -= token.utterance.startOffset;\n\t\t}\n\t\tself.wrapTextNodes(\n\t\t\tspan,\n\t\t\ttextNodes,\n\t\t\tstartOffset,\n\t\t\tendOffset\n\t\t);\n\t};\n\n\t/**\n\t * Get text node, within utterance highlighting, for an item.\n\t *\n\t * @param {Object} item The item to get text node for.\n\t */\n\n\tthis.getNodeInUtteranceHighlighting = function ( item ) {\n\t\t// Get the text node from the utterance highlighting that\n\t\t// wrapped the node for `textElement`.\n\t\tconst textNode = $( self.utteranceHighlightingSelector )\n\t\t\t.filter( function () {\n\t\t\t\treturn this.textPath ===\n\t\t\t\t\titem.path;\n\t\t\t} )\n\t\t\t.contents()\n\t\t\t.get( 0 );\n\t\treturn textNode;\n\t};\n\n\t/**\n\t * Set a timer for when the next token should be highlighted.\n\t *\n\t * @param {Object} token The original token. The timer is set\n\t * for the token following this one.\n\t */\n\n\tthis.setHighlightTokenTimer = function ( token ) {\n\t\tconst currentTime = token.utterance.audio.currentTime * 1000;\n\t\t// The duration of the timer is the duration of the\n\t\t// current token.\n\t\tconst duration = token.endTime - currentTime;\n\t\tconst nextToken = mw.wikispeech.storage.getNextToken( token );\n\t\tif ( nextToken ) {\n\t\t\tself.highlightTokenTimer = window.setTimeout(\n\t\t\t\t() => {\n\t\t\t\t\tself.removeWrappers(\n\t\t\t\t\t\t'.ext-wikispeech-highlight-word'\n\t\t\t\t\t);\n\t\t\t\t\tself.highlightToken( nextToken );\n\t\t\t\t\t// Add a new timer for the next token, when it\n\t\t\t\t\t// starts playing.\n\t\t\t\t\tself.setHighlightTokenTimer( nextToken );\n\t\t\t\t},\n\t\t\t\tduration / mw.user.options.get( 'wikispeechSpeechRate' )\n\t\t\t);\n\t\t}\n\t};\n\n\t/**\n\t * Remove elements wrapping text nodes.\n\t *\n\t * Restores the text nodes to the way they were before they\n\t * were wrapped.\n\t *\n\t * @param {string} wrapperSelector The selector for the\n\t * elements to remove\n\t */\n\n\tthis.removeWrappers = function ( wrapperSelector ) {\n\t\tconst parents = [];\n\t\tconst $span = $( wrapperSelector );\n\t\t$span.each( function () {\n\t\t\tparents.push( this.parentNode );\n\t\t} );\n\t\t$span.contents().unwrap();\n\t\tif ( parents.length > 0 ) {\n\t\t\t// Merge first and last text nodes, if the original was\n\t\t\t// divided by adding the <span>.\n\t\t\tparents[ 0 ].normalize();\n\t\t\tparents[ parents.length - 1 ].normalize();\n\t\t}\n\t};\n\n\t/**\n\t * Remove any sentence and word highlighting.\n\t */\n\n\tthis.clearHighlighting = function () {\n\t\t// Remove sentence highlighting.\n\t\tself.removeWrappers( '.ext-wikispeech-highlight-sentence' );\n\t\t// Remove word highlighting.\n\t\tself.removeWrappers( '.ext-wikispeech-highlight-word' );\n\t\tself.clearHighlightTokenTimer();\n\t};\n\n\t/**\n\t * Clear the timer for highlighting tokens.\n\t */\n\n\tthis.clearHighlightTokenTimer = function () {\n\t\tclearTimeout( self.highlightTokenTimer );\n\t};\n}\n\nmw.wikispeech = mw.wikispeech || {};\nmw.wikispeech.highlighter = new Highlighter();\nmw.wikispeech.Highlighter = Highlighter;\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.loader.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":10,"column":2,"nodeType":"CallExpression","endLine":12,"endColumn":5}],"suppressedMessages":[{"ruleId":"no-jquery/no-global-selector","severity":2,"message":"Avoid queries which search the entire DOM. Keep DOM nodes in memory where possible.","line":8,"column":1,"nodeType":"CallExpression","endLine":8,"endColumn":32,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * A small helper script to trigger the loading of the Wikispeech modules.\n *\n * @class ext.wikispeech.loader\n */\n\n// eslint-disable-next-line no-jquery/no-global-selector\n$( '.ext-wikispeech-listen a' ).one( 'click', () => {\n\tmw.log( '[Wikispeech] Loading Wikispeech...' );\n\tmw.loader.using( 'ext.wikispeech' ).done( () => {\n\t\tmw.log( '[Wikispeech] Loaded Wikispeech.' );\n\t} );\n} );\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.main.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":83,"column":1,"nodeType":"CallExpression","endLine":88,"endColumn":2}],"suppressedMessages":[{"ruleId":"no-jquery/no-global-selector","severity":2,"message":"Avoid queries which search the entire DOM. Keep DOM nodes in memory where possible.","line":33,"column":29,"nodeType":"CallExpression","endLine":33,"endColumn":60,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Main class for the Wikispeech extension.\n *\n * Handles setup of various components and initialization.\n *\n * @class ext.wikispeech.Main\n * @constructor\n */\n\nfunction Main() {\n\tconst self = this;\n\n\tthis.init = function () {\n\t\tif ( !self.enabledForNamespace() ) {\n\t\t\t// TODO: This is only required for tests to run\n\t\t\t// properly since namespace is checked at an earlier\n\t\t\t// stage for production code. See T267529.\n\t\t\treturn;\n\t\t}\n\n\t\tif ( mw.config.get( 'wgMFMode' ) ) {\n\t\t\t// Do not load Wikispeech if MobileFrontend is\n\t\t\t// enabled since it does not support its mobile\n\t\t\t// view. See T169059.\n\t\t\treturn;\n\t\t}\n\n\t\tmw.wikispeech.storage.loadUtterances( window );\n\t\t// Prepare the first utterance for playback.\n\t\tmw.wikispeech.ui.init();\n\t\t// Prepare action link.\n\t\t// eslint-disable-next-line no-jquery/no-global-selector\n\t\tconst $toggleVisibility = $( '.ext-wikispeech-listen a' );\n\t\t// Set label to hide message since the player is\n\t\t// visible when loaded.\n\t\t$toggleVisibility.text(\n\t\t\tmw.msg( 'wikispeech-dont-listen' )\n\t\t);\n\t\t$toggleVisibility.on(\n\t\t\t'click',\n\t\t\t$toggleVisibility,\n\t\t\tself.toggleVisibility\n\t\t);\n\t};\n\n\t/**\n\t * Toggle the visibility of the control panel.\n\t *\n\t * @param {Event} event\n\t */\n\n\tthis.toggleVisibility = function ( event ) {\n\t\tmw.wikispeech.ui.toggleVisibility();\n\n\t\tlet toggleVisibilityMessage;\n\t\tif ( mw.wikispeech.ui.isShown() ) {\n\t\t\ttoggleVisibilityMessage = 'wikispeech-dont-listen';\n\t\t} else {\n\t\t\ttoggleVisibilityMessage = 'wikispeech-listen';\n\t\t}\n\t\tconst $toggleVisibility = event.data;\n\t\t// Messages that can be used here:\n\t\t// * wikispeech-listen\n\t\t// * wikispeech-dont-listen\n\t\t$toggleVisibility.text( mw.msg( toggleVisibilityMessage ) );\n\t};\n\n\t/**\n\t * Check if Wikispeech is enabled for the current namespace.\n\t *\n\t * @return {boolean} true is the namespace of current page\n\t * should activate Wikispeech, else false.\n\t */\n\n\tthis.enabledForNamespace = function () {\n\t\tconst validNamespaces = mw.config.get( 'wgWikispeechNamespaces' );\n\t\tconst namespace = mw.config.get( 'wgNamespaceNumber' );\n\t\treturn validNamespaces.includes( namespace );\n\t};\n\n}\n\nmw.loader.using( [ 'mediawiki.api', 'ext.wikispeech' ] ).done(\n\t() => {\n\t\tconst main = new Main();\n\t\tmain.init();\n\t}\n);\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.player.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":54,"column":3,"nodeType":"CallExpression","endLine":58,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":99,"column":3,"nodeType":"CallExpression","endLine":104,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .fail","line":99,"column":3,"nodeType":"CallExpression","endLine":122,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":112,"column":5,"nodeType":"CallExpression","endLine":121,"endColumn":9}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Play, stop and navigate in the recitation.\n *\n * @class ext.wikispeech.Player\n * @constructor\n */\n\nfunction Player() {\n\tconst self = this;\n\tself.currentUtterance = null;\n\n\t/**\n\t * Play or stop, depending on whether an utterance is playing.\n\t */\n\n\tthis.playOrStop = function () {\n\t\tif ( self.isPlaying() ) {\n\t\t\tself.stop();\n\t\t} else {\n\t\t\tself.play();\n\t\t}\n\t};\n\n\t/**\n\t * Test if there currently is an utterance playing\n\t *\n\t * @return {boolean} true if there is an utterance playing,\n\t * else false.\n\t */\n\n\tthis.isPlaying = function () {\n\t\treturn self.currentUtterance !== null;\n\t};\n\n\t/**\n\t * Stop playing the utterance currently playing.\n\t */\n\n\tthis.stop = function () {\n\t\tif ( self.isPlaying() ) {\n\t\t\tself.stopUtterance( self.currentUtterance );\n\t\t}\n\t\tself.currentUtterance = null;\n\t\tmw.wikispeech.ui.setPlayStopIconToPlay();\n\t\tmw.wikispeech.ui.hideBufferingIcon();\n\t\tself.playingSelection = false;\n\t};\n\n\t/**\n\t * Start playing the first utterance or selected text, if any.\n\t */\n\n\tthis.play = function () {\n\t\tmw.wikispeech.storage.utterancesLoaded.done( () => {\n\t\t\tif ( !mw.wikispeech.selectionPlayer.playSelectionIfValid() ) {\n\t\t\t\tself.playUtterance( mw.wikispeech.storage.utterances[ 0 ] );\n\t\t\t}\n\t\t} );\n\t\tmw.wikispeech.ui.setPlayStopIconToStop();\n\t};\n\n\t/**\n\t * Play the audio for an utterance.\n\t *\n\t * This also stops any currently playing utterance.\n\t *\n\t * @param {Object} utterance The utterance to play the audio\n\t * for.\n\t * @param {boolean} [fromStart=true] Whether the utterance\n\t * should play from start or not.\n\t */\n\n\tthis.playUtterance = function ( utterance, fromStart ) {\n\t\tfromStart = fromStart === undefined ? true : fromStart;\n\t\tif ( fromStart && self.isPlaying() ) {\n\t\t\tself.stopUtterance( self.currentUtterance );\n\t\t}\n\t\tself.currentUtterance = utterance;\n\t\tif ( !self.playingSelection ) {\n\t\t\tmw.wikispeech.highlighter.highlightUtterance( utterance );\n\t\t}\n\t\tself.prepareAndPlayUtterance( utterance );\n\t\tmw.wikispeech.ui.showBufferingIconIfAudioIsLoading(\n\t\t\tutterance.audio\n\t\t);\n\t};\n\n\t/**\n\t * Ensure an utterance is ready for playback and play it.\n\t *\n\t * Plays utterance when it is ready. If the utterance fail to\n\t * prepare and it is currently playing, a popup dialog will\n\t * appear, letting the user retry or stop playback.\n\t *\n\t * @param {Object} utterance\n\t */\n\n\tthis.prepareAndPlayUtterance = function ( utterance ) {\n\t\tmw.wikispeech.storage.prepareUtterance( utterance )\n\t\t\t.done( () => {\n\t\t\t\tif ( utterance === self.currentUtterance ) {\n\t\t\t\t\tutterance.audio.play();\n\t\t\t\t}\n\t\t\t} )\n\t\t\t.fail( () => {\n\t\t\t\tif ( utterance !== self.currentUtterance ) {\n\t\t\t\t\t// Only show dialog if the current utterance\n\t\t\t\t\t// fails to load, to avoid multiple and less\n\t\t\t\t\t// relevant dialogs.\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tmw.wikispeech.ui.showLoadAudioError()\n\t\t\t\t\t.done( ( data ) => {\n\t\t\t\t\t\tif ( !data || data.action === 'stop' ) {\n\t\t\t\t\t\t\t// Stop both when \"Stop\" is clicked\n\t\t\t\t\t\t\t// and when escape is pressed.\n\t\t\t\t\t\t\tself.stop();\n\t\t\t\t\t\t} else if ( data.action === 'retry' ) {\n\t\t\t\t\t\t\tself.prepareAndPlayUtterance( utterance );\n\t\t\t\t\t\t}\n\t\t\t\t\t} );\n\t\t\t} );\n\t};\n\n\t/**\n\t * Stop and rewind the audio for an utterance.\n\t *\n\t * @param {Object} utterance The utterance to stop the audio\n\t * for.\n\t */\n\n\tthis.stopUtterance = function ( utterance ) {\n\t\tutterance.audio.pause();\n\t\t// Rewind audio for next time it plays.\n\t\tutterance.audio.currentTime = 0;\n\t\tmw.wikispeech.ui.removeCanPlayListener( $( utterance.audio ) );\n\t\tmw.wikispeech.highlighter.clearHighlighting();\n\t};\n\n\t/**\n\t * Skip to the next utterance.\n\t *\n\t * Stop the current utterance and start playing the next one.\n\t */\n\n\tthis.skipAheadUtterance = function () {\n\t\tconst nextUtterance =\n\t\t\tmw.wikispeech.storage.getNextUtterance( self.currentUtterance );\n\t\tif ( nextUtterance ) {\n\t\t\tself.playUtterance( nextUtterance );\n\t\t} else {\n\t\t\tself.stop();\n\t\t}\n\t};\n\n\t/**\n\t * Skip to the previous utterance.\n\t *\n\t * Stop the current utterance and start playing the previous\n\t * one. If the first utterance is playing, restart it.\n\t */\n\n\tthis.skipBackUtterance = function () {\n\t\tconst rewindThreshold = mw.config.get(\n\t\t\t'wgWikispeechSkipBackRewindsThreshold'\n\t\t);\n\t\tconst time = self.currentUtterance.audio.currentTime;\n\t\tif (\n\t\t\ttime > rewindThreshold ||\n\t\t\t\tself.currentUtterance === mw.wikispeech.storage.utterances[ 0 ]\n\t\t) {\n\t\t\t// Restart the current utterance if it's the first one\n\t\t\t// or if it has played for longer than the skip back\n\t\t\t// threshold. The threshold is based on position in\n\t\t\t// the audio, rather than time played. This means it\n\t\t\t// scales with speech rate.\n\t\t\tself.currentUtterance.audio.currentTime = 0;\n\t\t} else {\n\t\t\tconst previousUtterance =\n\t\t\t\tmw.wikispeech.storage.getPreviousUtterance(\n\t\t\t\t\tself.currentUtterance\n\t\t\t\t);\n\t\t\tself.playUtterance( previousUtterance );\n\t\t}\n\t};\n\n\t/**\n\t * Get the token being played.\n\t *\n\t * @return {Object} The token being played.\n\t */\n\n\tthis.getCurrentToken = function () {\n\t\tlet currentToken = null;\n\t\tconst tokens = self.currentUtterance.tokens;\n\t\tconst currentTime = self.currentUtterance.audio.currentTime * 1000;\n\t\tconst tokensWithDuration = tokens.filter( ( token ) => {\n\t\t\tconst duration = token.endTime - token.startTime;\n\t\t\treturn duration > 0;\n\t\t} );\n\t\tconst lastTokenWithDuration =\n\t\t\tmw.wikispeech.util.getLast( tokensWithDuration );\n\t\tif ( currentTime === lastTokenWithDuration.endTime ) {\n\t\t\t// If the current time is equal to the end time of the\n\t\t\t// last token, the last token is the current.\n\t\t\tcurrentToken = lastTokenWithDuration;\n\t\t} else {\n\t\t\tcurrentToken = tokensWithDuration.find( ( token ) => token.startTime <= currentTime &&\n\t\t\t\t\ttoken.endTime > currentTime );\n\t\t}\n\t\treturn currentToken;\n\t};\n\n\t/**\n\t * Skip to the next token.\n\t *\n\t * If there are no more tokens in the current utterance, skip\n\t * to the next utterance.\n\t */\n\n\tthis.skipAheadToken = function () {\n\t\tif ( self.isPlaying() ) {\n\t\t\tconst nextToken =\n\t\t\t\tmw.wikispeech.storage.getNextToken( self.getCurrentToken() );\n\t\t\tif ( !nextToken ) {\n\t\t\t\tself.skipAheadUtterance();\n\t\t\t} else {\n\t\t\t\tself.currentUtterance.audio.currentTime = nextToken.startTime / 1000;\n\t\t\t\tmw.wikispeech.highlighter.startTokenHighlighting(\n\t\t\t\t\tnextToken\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Skip to the previous token.\n\t *\n\t * If there are no preceding tokens, skip to the last token of\n\t * the previous utterance.\n\t */\n\n\tthis.skipBackToken = function () {\n\t\tif ( self.isPlaying() ) {\n\t\t\tlet previousToken =\n\t\t\t\tmw.wikispeech.storage.getPreviousToken( self.getCurrentToken() );\n\t\t\tif ( !previousToken ) {\n\t\t\t\tself.skipBackUtterance();\n\t\t\t\tpreviousToken =\n\t\t\t\t\tmw.wikispeech.storage.getLastToken( self.currentUtterance );\n\t\t\t}\n\t\t\tself.currentUtterance.audio.currentTime = previousToken.startTime / 1000;\n\t\t\tmw.wikispeech.highlighter.startTokenHighlighting(\n\t\t\t\tpreviousToken\n\t\t\t);\n\t\t}\n\t};\n}\n\nmw.wikispeech = mw.wikispeech || {};\nmw.wikispeech.Player = Player;\nmw.wikispeech.player = new Player();\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.selectionPlayer.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":144,"column":3,"nodeType":"CallExpression","endLine":155,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":179,"column":3,"nodeType":"CallExpression","endLine":191,"endColumn":7}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * The player that appears when the user selects a bit of text.\n *\n * Includes logic for finding what to play, and starting and\n * stopping within an utterance.\n *\n * @class ext.wikispeech.SelectionPlayer\n * @constructor\n */\n\nfunction SelectionPlayer() {\n\tconst self = this;\n\tself.previousEndUtterance = null;\n\n\t/**\n\t * Play selected text if selection is valid.\n\t *\n\t * @return {boolean} true selection plays, else false.\n\t */\n\n\tthis.playSelectionIfValid = function () {\n\t\tif ( self.isSelectionValid() ) {\n\t\t\tself.playSelection();\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t};\n\n\t/**\n\t * Test if the selected text is valid for recitation.\n\t *\n\t * Valid here means that the start and end points of the\n\t * selection are in nodes which are part of utterances. Nodes\n\t * outside utterances may occur within the selection.\n\t *\n\t * @return {boolean} true if the selection is valid for\n\t * recitation, else false.\n\t */\n\n\tthis.isSelectionValid = function () {\n\t\tif ( !self.isTextSelected() ) {\n\t\t\treturn false;\n\t\t}\n\t\tconst firstNode = self.getFirstNodeInSelection();\n\t\tconst firstTextNode =\n\t\t\tmw.wikispeech.storage.getFirstTextNode( firstNode, true );\n\t\tconst lastNode = self.getLastNodeInSelection();\n\t\tconst lastTextNode =\n\t\t\tmw.wikispeech.storage.getLastTextNode( lastNode, true );\n\t\tif (\n\t\t\tmw.wikispeech.storage.isNodeInUtterance( firstTextNode ) &&\n\t\t\t\tmw.wikispeech.storage.isNodeInUtterance( lastTextNode )\n\t\t) {\n\t\t\treturn true;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t};\n\n\t/**\n\t * Test if there is any selected text.\n\t *\n\t * @return {boolean} true if there is any text selected, else false.\n\t */\n\n\tthis.isTextSelected = function () {\n\t\tconst selection = window.getSelection();\n\t\treturn !selection.isCollapsed;\n\t};\n\n\t/**\n\t * Get the first node in the selection.\n\t *\n\t * Corrects node and offset that is sometimes incorrect in Firefox.\n\t *\n\t * @return {Text} The first node in the selection.\n\t */\n\n\tthis.getFirstNodeInSelection = function () {\n\t\tconst selection = window.getSelection();\n\t\tconst startRange = selection.getRangeAt( 0 );\n\t\tconst startNode = startRange.startContainer;\n\t\tif (\n\t\t\tstartNode.nodeType === 3 &&\n\t\t\t\tstartRange.startOffset === startNode.textContent.length\n\t\t) {\n\t\t\t// Check if start offset is beyond the end of the text node.\n\t\t\t// This is needed because of a bug in Firefox that\n\t\t\t// causes incorrect selections, when double clicking\n\t\t\t// selects the start or end of a text node. See:\n\t\t\t// https://bugzilla.mozilla.org/show_bug.cgi?id=1298845\n\t\t\tlet nodeBeforeActualNode = startNode;\n\t\t\twhile ( !nodeBeforeActualNode.nextSibling ) {\n\t\t\t\tnodeBeforeActualNode = nodeBeforeActualNode.parentNode;\n\t\t\t}\n\t\t\treturn nodeBeforeActualNode.nextSibling;\n\t\t} else {\n\t\t\treturn startNode;\n\t\t}\n\t};\n\n\t/**\n\t * Play selected text.\n\t *\n\t * Plays utterances containing the selected text. The first\n\t * utterance starts playing at the first token that is\n\t * selected and the last utterance stops playing after the\n\t * last.\n\t */\n\n\tthis.playSelection = function () {\n\t\tmw.wikispeech.player.playingSelection = true;\n\t\tconst selection = window.getSelection();\n\t\tconst startRange = selection.getRangeAt( 0 );\n\t\tconst firstSelectionNode = self.getFirstNodeInSelection();\n\t\tlet startOffset;\n\t\tif (\n\t\t\tfirstSelectionNode !== startRange.startContainer ||\n\t\t\t\tfirstSelectionNode.nodeType === 1\n\t\t) {\n\t\t\t// If the start node has been changed, this is because\n\t\t\t// it was corrected in getFirstNodeInSelection(). If\n\t\t\t// this is the case, the selection actually starts at\n\t\t\t// the beginning of the current node. If the start\n\t\t\t// node is an element, start offset is also zero,\n\t\t\t// because if should start in the first child text\n\t\t\t// nodes of that node.\n\t\t\tstartOffset = 0;\n\t\t} else {\n\t\t\tstartOffset = startRange.startOffset;\n\t\t}\n\t\tconst startNode =\n\t\t\tmw.wikispeech.storage.getFirstTextNode(\n\t\t\t\tfirstSelectionNode,\n\t\t\t\ttrue\n\t\t\t);\n\t\tconst startUtterance =\n\t\t\tmw.wikispeech.storage.getStartUtterance(\n\t\t\t\tstartNode,\n\t\t\t\tstartOffset\n\t\t\t);\n\t\tmw.wikispeech.player.currentUtterance = startUtterance;\n\t\tmw.wikispeech.storage.prepareUtterance(\n\t\t\tstartUtterance\n\t\t)\n\t\t\t.done( () => {\n\t\t\t\tconst startToken = mw.wikispeech.storage.getStartToken(\n\t\t\t\t\tstartUtterance,\n\t\t\t\t\tstartNode,\n\t\t\t\t\tstartOffset\n\t\t\t\t);\n\t\t\t\tself.setStartTime( startUtterance, startToken.startTime );\n\t\t\t\tmw.wikispeech.player.playUtterance( startUtterance, false );\n\t\t\t} );\n\t\tmw.wikispeech.ui.showBufferingIconIfAudioIsLoading(\n\t\t\tstartUtterance.audio\n\t\t);\n\n\t\tconst endRange = selection.getRangeAt( selection.rangeCount - 1 );\n\t\tconst lastSelectionNode = self.getLastNodeInSelection();\n\t\tlet endOffset;\n\t\tif (\n\t\t\tlastSelectionNode !== endRange.endContainer ||\n\t\t\t\tlastSelectionNode.nodeType === 1\n\t\t) {\n\t\t\tendOffset = lastSelectionNode.textContent.length - 1;\n\t\t} else {\n\t\t\tendOffset = endRange.endOffset - 1;\n\t\t}\n\t\tconst endNode =\n\t\t\tmw.wikispeech.storage.getLastTextNode(\n\t\t\t\tlastSelectionNode,\n\t\t\t\ttrue\n\t\t\t);\n\t\tconst endUtterance =\n\t\t\tmw.wikispeech.storage.getEndUtterance( endNode, endOffset );\n\t\tself.previousEndUtterance = endUtterance;\n\t\tmw.wikispeech.storage.prepareUtterance(\n\t\t\tendUtterance\n\t\t)\n\t\t\t.done( () => {\n\t\t\t\t// Prepare the end utterance, since token information\n\t\t\t\t// is needed to calculate the correct end token.\n\t\t\t\tconst endToken = mw.wikispeech.storage.getEndToken(\n\t\t\t\t\tendUtterance,\n\t\t\t\t\tendNode,\n\t\t\t\t\tendOffset\n\t\t\t\t);\n\t\t\t\tself.setEndTime( endUtterance, endToken.endTime );\n\t\t\t} );\n\t};\n\n\t/**\n\t * Set the time where an utterance will start playing.\n\t *\n\t * @param {Object} utterance The utterance to set start time\n\t * for.\n\t * @param {number} startTime The time in milliseconds\n\t * to start playing at.\n\t */\n\n\tthis.setStartTime = function ( utterance, startTime ) {\n\t\tutterance.audio.currentTime = startTime / 1000;\n\t};\n\n\t/**\n\t * Get the last node in the selection.\n\t *\n\t * Corrects node and offset that is sometimes incorrect in\n\t * Firefox.\n\t *\n\t * @return {Text} The last node in the selection.\n\t */\n\n\tthis.getLastNodeInSelection = function () {\n\t\tconst selection = window.getSelection();\n\t\tconst endRange = selection.getRangeAt( selection.rangeCount - 1 );\n\t\tconst endNode = endRange.endContainer;\n\t\tif (\n\t\t\tendNode.nodeType === 3 &&\n\t\t\t\tendRange.endOffset === 0\n\t\t) {\n\t\t\t// Check if end offset is zero. This is needed\n\t\t\t// because of a bug in Firefox that causes incorrect\n\t\t\t// selections, when double clicking selects the start\n\t\t\t// or end of a text node. See:\n\t\t\t// https://bugzilla.mozilla.org/show_bug.cgi?id=1298845\n\t\t\tlet nodeAfterActualNode = endNode;\n\t\t\twhile ( !nodeAfterActualNode.previousSibling ) {\n\t\t\t\tnodeAfterActualNode = nodeAfterActualNode.parentNode;\n\t\t\t}\n\t\t\treturn nodeAfterActualNode.previousSibling;\n\t\t} else {\n\t\t\treturn endNode;\n\t\t}\n\t};\n\n\t/**\n\t * Set the time where an utterance will stop playing.\n\t *\n\t * Create an event handler for when the utterance starts\n\t * playing. The handler creates a timeout that triggers when\n\t * the end time is reached, stopping playback.\n\t *\n\t * @param {Object} utterance The utterance to set end time for.\n\t * @param {number} endTime The time in milliseconds to stop\n\t * playing after.\n\t */\n\n\tthis.setEndTime = function ( utterance, endTime ) {\n\t\t$( utterance.audio ).one( 'playing.end', () => {\n\t\t\tconst timeLeft = endTime - utterance.audio.currentTime * 1000;\n\t\t\tutterance.stopTimeout =\n\t\t\t\twindow.setTimeout(\n\t\t\t\t\t() => {\n\t\t\t\t\t\tmw.wikispeech.player.stop();\n\t\t\t\t\t\tself.resetPreviousEndUtterance();\n\t\t\t\t\t},\n\t\t\t\t\ttimeLeft / mw.user.options.get( 'wikispeechSpeechRate' )\n\t\t\t\t);\n\t\t} );\n\t};\n\n\t/**\n\t * Remove timeout for stopping end utterance.\n\t */\n\n\tthis.resetPreviousEndUtterance = function () {\n\t\tif ( self.previousEndUtterance ) {\n\t\t\t// Remove any trigger for setting end time for an\n\t\t\t// utterance. Otherwise, this will trigger the next\n\t\t\t// time the utterance is the end utterance, possibly\n\t\t\t// stopping playback too early.\n\t\t\t$( self.previousEndUtterance.audio ).off( 'playing.end' );\n\t\t\twindow.clearTimeout( self.previousEndUtterance.stopTimeout );\n\t\t\tself.previousEndUtterance.stopTimeout = null;\n\t\t\tself.previousEndUtterance = null;\n\t\t}\n\t};\n}\n\nmw.wikispeech = mw.wikispeech || {};\nmw.wikispeech.selectionPlayer = new SelectionPlayer();\nmw.wikispeech.SelectionPlayer = SelectionPlayer;\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.specialEditLexicon.js","messages":[],"suppressedMessages":[{"ruleId":"no-jquery/no-global-selector","severity":2,"message":"Avoid queries which search the entire DOM. Keep DOM nodes in memory where possible.","line":3,"column":18,"nodeType":"CallExpression","endLine":3,"endColumn":41,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.storage.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":41,"column":3,"nodeType":"CallExpression","endLine":74,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":153,"column":10,"nodeType":"CallExpression","endLine":166,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":197,"column":19,"nodeType":"CallExpression","endLine":209,"endColumn":7}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Loads and stores objects used by the extension.\n *\n * Contains functions for other modules to retrieve information\n * about the utterances.\n *\n * @class ext.wikispeech.Storage\n * @constructor\n */\n\nfunction Storage() {\n\tconst self = this;\n\tself.utterances = [];\n\tself.utterancesLoaded = $.Deferred();\n\n\tif ( mw.wikispeech.consumerMode ) {\n\t\tconst producerApiUrl = mw.wikispeech.producerUrl + '/api.php';\n\t\tself.api = new mw.ForeignApi( producerApiUrl );\n\t} else {\n\t\tself.api = new mw.Api();\n\t}\n\n\t/**\n\t * Load all utterances.\n\t *\n\t * Uses the MediaWiki API to get the segments of the text.\n\t *\n\t * @param {Object} window\n\t */\n\n\tthis.loadUtterances = function ( window ) {\n\t\tconst page = mw.config.get( 'wgPageName' );\n\t\tconst options = {\n\t\t\taction: 'wikispeech-segment',\n\t\t\tpage: page\n\t\t};\n\t\tif ( mw.wikispeech.consumerMode ) {\n\t\t\toptions[ 'consumer-url' ] = window.location.origin +\n\t\t\t\tmw.config.get( 'wgScriptPath' );\n\t\t}\n\t\tself.api.get(\n\t\t\toptions,\n\t\t\t{\n\t\t\t\tbeforeSend: function ( jqXHR, settings ) {\n\t\t\t\t\tmw.log(\n\t\t\t\t\t\t'Requesting segments:', settings.url\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t).done( ( data ) => {\n\t\t\tmw.log( 'Segments received:', data );\n\t\t\tself.utterances = data[ 'wikispeech-segment' ].segments;\n\n\t\t\t// Add extra offset to the title if it has leading\n\t\t\t// whitespaces. When using the new skin, there are\n\t\t\t// whitespaces around the title that do not appear in\n\t\t\t// the display title. This leads to highlighting being\n\t\t\t// wrong.\n\t\t\tconst titleUtterance = self.utterances[ 0 ];\n\t\t\tconst firstNode = self.getNodeForItem( titleUtterance.content[ 0 ] );\n\t\t\tconst leadingWhitespaces = firstNode.textContent.match( /^\\s+/ );\n\t\t\tif ( leadingWhitespaces ) {\n\t\t\t\tconst offset = leadingWhitespaces[ 0 ].length;\n\t\t\t\ttitleUtterance.startOffset += offset;\n\t\t\t\ttitleUtterance.endOffset += offset;\n\t\t\t}\n\n\t\t\tfor ( let i = 0; i < self.utterances.length; i++ ) {\n\t\t\t\tconst utterance = self.utterances[ i ];\n\t\t\t\tutterance.audio = $( '<audio>' ).get( 0 );\n\t\t\t}\n\t\t\tself.utterancesLoaded.resolve();\n\t\t\tself.prepareUtterance( self.utterances[ 0 ] );\n\t\t} );\n\t};\n\n\t/**\n\t * Prepare an utterance for playback.\n\t *\n\t * Audio for the utterance is requested from the Speechoid service\n\t * and event listeners are added. When an utterance starts\n\t * playing, the next one is prepared, and when an utterance is\n\t * done, the next utterance is played. This is meant to be a\n\t * balance between not having to pause between utterance and\n\t * not requesting more than needed.\n\t *\n\t * @param {Object} utterance The utterance to prepare.\n\t * @return {jQuery.Promise}\n\t */\n\n\tthis.prepareUtterance = function ( utterance ) {\n\t\tconst $audio = $( utterance.audio );\n\t\tif ( !utterance.request ) {\n\t\t\t// Add event listener only once.\n\t\t\t$audio.on( 'playing', () => {\n\t\t\t\tlet firstToken;\n\n\t\t\t\t// Highlight token only when the audio starts\n\t\t\t\t// playing, since we need the token info from the\n\t\t\t\t// response to know what to highlight.\n\t\t\t\tif (\n\t\t\t\t\t!mw.wikispeech.player.playingSelection &&\n\t\t\t\t\t\t$audio.prop( 'currentTime' ) === 0\n\t\t\t\t) {\n\t\t\t\t\tfirstToken = utterance.tokens[ 0 ];\n\t\t\t\t\tmw.wikispeech.highlighter.startTokenHighlighting(\n\t\t\t\t\t\tfirstToken\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} );\n\t\t\tconst nextUtterance = self.getNextUtterance( utterance );\n\t\t\tif ( nextUtterance ) {\n\t\t\t\t$audio.on( {\n\t\t\t\t\tplay: function () {\n\t\t\t\t\t\tself.prepareUtterance( nextUtterance );\n\t\t\t\t\t},\n\t\t\t\t\tended: function () {\n\t\t\t\t\t\tmw.wikispeech.player.skipAheadUtterance();\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} else {\n\t\t\t\t// For last utterance, just stop the playback when\n\t\t\t\t// done.\n\t\t\t\t$audio.on( 'ended', () => {\n\t\t\t\t\tmw.wikispeech.player.stop();\n\t\t\t\t} );\n\t\t\t}\n\t\t}\n\t\tif ( !utterance.request || utterance.request.state() === 'rejected' ) {\n\t\t\t// Only load audio for an utterance if it hasn't been\n\t\t\t// successfully loaded yet.\n\t\t\tutterance.request = self.loadAudio( utterance );\n\t\t}\n\t\treturn utterance.request;\n\t};\n\n\t/**\n\t * Load audio for an utterance.\n\t *\n\t * Sends a request to the Speechoid service and adds audio and tokens\n\t * when the response is received.\n\t *\n\t * @param {Object} utterance The utterance to load audio for.\n\t * @return {jQuery.Promise}\n\t */\n\n\tthis.loadAudio = function ( utterance ) {\n\t\tconst utteranceIndex = self.utterances.indexOf( utterance );\n\t\tmw.log(\n\t\t\t'Loading audio for utterance #' + utteranceIndex + ':',\n\t\t\tutterance\n\t\t);\n\t\treturn self.requestTts( utterance.hash, window )\n\t\t\t.done( ( response ) => {\n\t\t\t\tconst audioUrl = 'data:audio/ogg;base64,' +\n\t\t\t\t\tresponse[ 'wikispeech-listen' ].audio;\n\t\t\t\tmw.log(\n\t\t\t\t\t'Setting audio url for: [' + utteranceIndex + ']',\n\t\t\t\t\tutterance, '=',\n\t\t\t\t\tresponse[ 'wikispeech-listen' ].audio.length + ' base64 bytes'\n\t\t\t\t);\n\t\t\t\t$( utterance.audio ).attr( 'src', audioUrl );\n\t\t\t\tutterance.audio.playbackRate =\n\t\t\t\t\tmw.user.options.get( 'wikispeechSpeechRate' );\n\t\t\t\tself.addTokens( utterance, response[ 'wikispeech-listen' ].tokens );\n\t\t\t} );\n\t};\n\n\t/**\n\t * Send a request to the Speechoid service.\n\t *\n\t * Request is sent via the \"wikispeech-listen\" API action. Language to\n\t * use is retrieved from the current page.\n\t *\n\t * @param {string} segmentHash\n\t * @param {Object} window\n\t * @return {jQuery.Promise}\n\t */\n\n\tthis.requestTts = function ( segmentHash, window ) {\n\t\tconst language = mw.config.get( 'wgPageContentLanguage' );\n\t\tconst voice = mw.wikispeech.util.getUserVoice( language );\n\t\tconst options = {\n\t\t\taction: 'wikispeech-listen',\n\t\t\tlang: language,\n\t\t\trevision: mw.config.get( 'wgRevisionId' ),\n\t\t\tsegment: segmentHash\n\t\t};\n\t\tif ( voice !== '' ) {\n\t\t\t// Set voice if not default.\n\t\t\toptions.voice = voice;\n\t\t}\n\t\tif ( mw.wikispeech.consumerMode ) {\n\t\t\toptions[ 'consumer-url' ] = window.location.origin +\n\t\t\t\tmw.config.get( 'wgScriptPath' );\n\t\t}\n\t\tconst request = self.api.get(\n\t\t\toptions,\n\t\t\t{\n\t\t\t\tbeforeSend: function ( jqXHR, settings ) {\n\t\t\t\t\tmw.log(\n\t\t\t\t\t\t'Sending TTS request: ' + settings.url\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\t\t\t.done( ( data ) => {\n\t\t\t\tmw.log( 'Response received:', data );\n\t\t\t} );\n\t\treturn request;\n\t};\n\n\t/**\n\t * Add tokens to an utterance.\n\t *\n\t * @param {Object} utterance The utterance to add tokens to.\n\t * @param {Object[]} responseTokens Tokens from a Speechoid response,\n\t * where each token is an object. For these objects, the\n\t * property \"orth\" is the string used by the TTS to generate\n\t * audio for the token.\n\t */\n\n\tthis.addTokens = function ( utterance, responseTokens ) {\n\t\tutterance.tokens = [];\n\t\tlet searchOffset = 0;\n\t\tfor ( let i = 0; i < responseTokens.length; i++ ) {\n\t\t\tconst responseToken = responseTokens[ i ];\n\t\t\tlet startTime;\n\t\t\tif ( i === 0 ) {\n\t\t\t\t// The first token in an utterance always start on\n\t\t\t\t// time zero.\n\t\t\t\tstartTime = 0;\n\t\t\t} else {\n\t\t\t\t// Since the response only contains end times for\n\t\t\t\t// token, the start time for a token is set to the\n\t\t\t\t// end time of the previous one.\n\t\t\t\tstartTime = responseTokens[ i - 1 ].endtime;\n\t\t\t}\n\t\t\tconst token = {\n\t\t\t\tstring: responseToken.orth,\n\t\t\t\tstartTime: startTime,\n\t\t\t\tendTime: responseToken.endtime,\n\t\t\t\tutterance: utterance\n\t\t\t};\n\t\t\tutterance.tokens.push( token );\n\t\t\tif ( i > 0 ) {\n\t\t\t\t// Start looking for the next token after the\n\t\t\t\t// previous one, except for the first token, where\n\t\t\t\t// we want to start on zero.\n\t\t\t\tsearchOffset += 1;\n\t\t\t}\n\t\t\tsearchOffset = self.addOffsetsAndItems(\n\t\t\t\ttoken,\n\t\t\t\tsearchOffset\n\t\t\t);\n\t\t}\n\t};\n\n\t/**\n\t * Add properties for offsets and items to a token.\n\t *\n\t * The offsets are for the start and end of the token in the\n\t * text node which they appear. These text nodes are not\n\t * necessary the same.\n\t *\n\t * The items store information used to get the text nodes in\n\t * which the token starts, ends and any text nodes in between.\n\t *\n\t * @param {Object} token The token to add properties to.\n\t * @param {number} searchOffset The offset to start searching\n\t * from, in the concatenated string.\n\t * @return {number} The end offset in the concatenated string.\n\t */\n\n\tthis.addOffsetsAndItems = function (\n\t\ttoken,\n\t\tsearchOffset\n\t) {\n\t\tconst utterance = token.utterance;\n\t\tlet items = [];\n\t\tconst startOffsetInUtteranceString =\n\t\t\tself.getStartOffsetInUtteranceString(\n\t\t\t\ttoken.string,\n\t\t\t\tutterance.content,\n\t\t\t\titems,\n\t\t\t\tsearchOffset\n\t\t\t);\n\t\tconst endOffsetInUtteranceString =\n\t\t\tstartOffsetInUtteranceString +\n\t\t\ttoken.string.length - 1;\n\n\t\t// `items` now contains all the items in the utterance,\n\t\t// from the first one to the last, that contains at least\n\t\t// part of the token. To get only the ones that contain\n\t\t// part of the token, the items that appear before the\n\t\t// token are removed.\n\t\tlet endOffsetForItem = 0;\n\t\titems =\n\t\t\titems.filter( ( item ) => {\n\t\t\t\tendOffsetForItem += item.string.length;\n\t\t\t\treturn endOffsetForItem >\n\t\t\t\t\tstartOffsetInUtteranceString;\n\t\t\t} );\n\t\ttoken.items = items;\n\n\t\t// Calculate start and end offset for the token, in the\n\t\t// text nodes it appears in, and add them to the\n\t\t// token.\n\t\tconst firstItemIndex =\n\t\t\tutterance.content.indexOf( items[ 0 ] );\n\t\tconst itemsBeforeStart =\n\t\t\tutterance.content.slice( 0, firstItemIndex );\n\t\tlet itemsBeforeStartLength = 0;\n\t\titemsBeforeStart.forEach( ( item ) => {\n\t\t\titemsBeforeStartLength += item.string.length;\n\t\t} );\n\t\ttoken.startOffset =\n\t\t\tstartOffsetInUtteranceString -\n\t\t\titemsBeforeStartLength;\n\t\tif ( token.items[ 0 ] === utterance.content[ 0 ] ) {\n\t\t\ttoken.startOffset += utterance.startOffset;\n\t\t}\n\t\tconst lastItemIndex =\n\t\t\tutterance.content.indexOf(\n\t\t\t\tmw.wikispeech.util.getLast( items )\n\t\t\t);\n\t\tconst itemsBeforeEnd = utterance.content.slice( 0, lastItemIndex );\n\t\tlet itemsBeforeEndLength = 0;\n\t\titemsBeforeEnd.forEach( ( item ) => {\n\t\t\titemsBeforeEndLength += item.string.length;\n\t\t} );\n\t\ttoken.endOffset =\n\t\t\tendOffsetInUtteranceString - itemsBeforeEndLength;\n\t\tif (\n\t\t\tmw.wikispeech.util.getLast( token.items ) ===\n\t\t\t\tutterance.content[ 0 ]\n\t\t) {\n\t\t\ttoken.endOffset += utterance.startOffset;\n\t\t}\n\t\treturn endOffsetInUtteranceString;\n\t};\n\n\t/**\n\t * Calculate the start offset of a token in the utterance string.\n\t *\n\t * The token is the first match found, starting at\n\t * searchOffset.\n\t *\n\t * @param {string} token The token to search for.\n\t * @param {Object[]} content The content of the utterance where\n\t * the token appear.\n\t * @param {Object[]} items An array of items to which each\n\t * item, up to and including the last one that contains\n\t * part of the token, is added.\n\t * @param {number} searchOffset Where we want to start looking\n\t * for the token in the utterance string.\n\t * @return {number} The offset where the first character of\n\t * the token appears in the utterance string.\n\t */\n\n\tthis.getStartOffsetInUtteranceString = function (\n\t\ttoken,\n\t\tcontent,\n\t\titems,\n\t\tsearchOffset\n\t) {\n\t\tlet startOffsetInUtteranceString, stringBeforeReplace;\n\n\t\t// The concatenation of the strings from items. Used to\n\t\t// find tokens that span multiple text nodes.\n\t\tlet concatenatedText = '';\n\t\tcontent.every( ( item ) => {\n\t\t\t// Look through the items until we find a substring\n\t\t\t// matching the token.\n\t\t\t// The `replaceAll` replaces non-breaking space with a\n\t\t\t// normal space. This is required if Speechoid returns\n\t\t\t// normal spaces in \"orth\" for a token. See\n\t\t\t// https://phabricator.wikimedia.org/T286997\n\t\t\tconcatenatedText += item.string.replace( ' ', ' ' );\n\n\t\t\t// Eslint does not allow replaceAll().\n\t\t\tdo {\n\t\t\t\tstringBeforeReplace = concatenatedText;\n\t\t\t\tconcatenatedText = concatenatedText.replace( ' ', ' ' );\n\t\t\t} while ( stringBeforeReplace !== concatenatedText );\n\n\t\t\titems.push( item );\n\t\t\tif ( searchOffset > concatenatedText.length ) {\n\t\t\t\t// Don't look in text elements that end before\n\t\t\t\t// where we start looking.\n\t\t\t\t// continue\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tstartOffsetInUtteranceString = concatenatedText.indexOf(\n\t\t\t\ttoken, searchOffset\n\t\t\t);\n\t\t\tif ( startOffsetInUtteranceString >= 0 ) {\n\t\t\t\t// break\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t} );\n\t\treturn startOffsetInUtteranceString;\n\t};\n\n\t/**\n\t * Get the utterance after the given utterance.\n\t *\n\t * @param {Object} utterance The original utterance.\n\t * @return {Object} The utterance after the original\n\t * utterance. null if utterance is the last one.\n\t */\n\n\tthis.getNextUtterance = function ( utterance ) {\n\t\treturn self.getUtteranceByOffset( utterance, 1 );\n\t};\n\n\t/**\n\t * Get the utterance by offset from another utterance.\n\t *\n\t * @param {Object} utterance The original utterance.\n\t * @param {number} offset The difference, in index, to the\n\t * wanted utterance. Can be negative for preceding\n\t * utterances.\n\t * @return {Object} The utterance on the position before or\n\t * after the original utterance, as specified by\n\t * `offset`. null if the original utterance is null.\n\t */\n\n\tthis.getUtteranceByOffset = function ( utterance, offset ) {\n\t\tif ( utterance === null ) {\n\t\t\treturn null;\n\t\t}\n\t\tconst index = self.utterances.indexOf( utterance );\n\t\treturn self.utterances[ index + offset ];\n\t};\n\n\t/**\n\t * Get the utterance before the given utterance.\n\t *\n\t * @param {Object} utterance The original utterance.\n\t * @return {Object} The utterance before the original\n\t * utterance. null if the original utterance is the\n\t * first one.\n\t */\n\n\tthis.getPreviousUtterance = function ( utterance ) {\n\t\treturn self.getUtteranceByOffset( utterance, -1 );\n\t};\n\n\t/**\n\t * Get the token following a given token.\n\t *\n\t * @param {Object} originalToken Find the next token after\n\t * this one.\n\t * @return {Object} The first token following originalToken\n\t * that has time greater than zero and a transcription. null\n\t * if no such token is found. Will not look beyond\n\t * originalToken's utterance.\n\t */\n\n\tthis.getNextToken = function ( originalToken ) {\n\t\tconst index = originalToken.utterance.tokens.indexOf( originalToken );\n\t\tconst succeedingTokens =\n\t\t\toriginalToken.utterance.tokens.slice( index + 1 ).filter(\n\t\t\t\t( token ) => !self.isSilent( token ) );\n\t\tif ( succeedingTokens.length === 0 ) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\treturn succeedingTokens[ 0 ];\n\t\t}\n\t};\n\n\t/**\n\t * Test if a token is silent.\n\t *\n\t * Silent is here defined as either having no transcription\n\t * (i.e. the empty string) or having no duration (i.e. start\n\t * and end time is the same).\n\t *\n\t * @param {Object} token The token to test.\n\t * @return {boolean} true if the token is silent, else false.\n\t */\n\n\tthis.isSilent = function ( token ) {\n\t\treturn token.startTime === token.endTime ||\n\t\t\ttoken.string === '';\n\t};\n\n\t/**\n\t * Get the token preceding a given token.\n\t *\n\t * @param {Object} originalToken Find the token before this one.\n\t * @return {Object} The first token following originalToken\n\t * that has time greater than zero and a transcription. null\n\t * if no such token is found. Will not look beyond\n\t * originalToken's utterance.\n\t */\n\n\tthis.getPreviousToken = function ( originalToken ) {\n\t\tconst index = originalToken.utterance.tokens.indexOf( originalToken );\n\t\tconst precedingTokens =\n\t\t\toriginalToken.utterance.tokens.slice( 0, index ).filter(\n\t\t\t\t( token ) => !self.isSilent( token ) );\n\t\tif ( precedingTokens.length === 0 ) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\tconst previousToken = mw.wikispeech.util.getLast( precedingTokens );\n\t\t\treturn previousToken;\n\t\t}\n\t};\n\n\t/**\n\t * Get the last non silent token in an utterance.\n\t *\n\t * @param {Object} utterance The utterance to get the last\n\t * token from.\n\t * @return {Object} The last token in the utterance.\n\t */\n\n\tthis.getLastToken = function ( utterance ) {\n\t\tconst nonSilentTokens = utterance.tokens.filter( ( token ) => !self.isSilent( token ) );\n\t\tconst lastToken = mw.wikispeech.util.getLast( nonSilentTokens );\n\t\treturn lastToken;\n\t};\n\n\t/**\n\t * Get the first text node that is a descendant of the given node.\n\t *\n\t * Finds the depth first text node, i.e. in\n\t * `<a><b>1</b>2</a>`\n\t * the node with text \"1\" is the first one. If the given node is\n\t * itself a text node, it is simply returned.\n\t *\n\t * @param {HTMLElement} node The node under which to look for\n\t * text nodes.\n\t * @param {boolean} inUtterance If true, the first text node\n\t * that is also in an utterance is returned.\n\t * @return {Text} The first text node under `node`,\n\t * undefined if there are no text nodes.\n\t */\n\n\tthis.getFirstTextNode = function ( node, inUtterance ) {\n\t\tif ( node.nodeType === 3 ) {\n\t\t\tif ( !inUtterance || self.isNodeInUtterance( node ) ) {\n\t\t\t\t// The given node is a text node. Check whether\n\t\t\t\t// the node is in an utterance, if that is\n\t\t\t\t// requested.\n\t\t\t\treturn node;\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( let i = 0; i < node.childNodes.length; i++ ) {\n\t\t\t\t// Check children if the given node is an element.\n\t\t\t\tconst child = node.childNodes[ i ];\n\t\t\t\tconst textNode = self.getFirstTextNode( child, inUtterance );\n\t\t\t\tif ( textNode ) {\n\t\t\t\t\treturn textNode;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Check if a text node is in any utterance.\n\t *\n\t * Utterances don't have any direct references to nodes, but\n\t * rather use XPath expressions to find the nodes that were used\n\t * when creating them.\n\t *\n\t * @param {Text} node The text node to check.\n\t * @return {boolean} true if the node is in any utterance, else false.\n\t */\n\n\tthis.isNodeInUtterance = function ( node ) {\n\t\tfor ( let i = 0; i < self.utterances.length; i++ ) {\n\t\t\tconst utterance = self.utterances[ i ];\n\t\t\tfor ( let j = 0; j < utterance.content.length; j++ ) {\n\t\t\t\tconst item = utterance.content[ j ];\n\t\t\t\tif ( self.getNodeForItem( item ) === node ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\t/**\n\t * Get the utterance containing a point, searching forward.\n\t *\n\t * Finds the utterance that contains a point in the text,\n\t * specified by a node and an offset in that node. Several\n\t * utterances may contain parts of the same node, which is why\n\t * the offset is needed.\n\t *\n\t * If the offset can't be found in the given node, later nodes\n\t * are checked. This happens if the offset falls between two\n\t * utterances.\n\t *\n\t * @param {Text} node The first node to check.\n\t * @param {number} offset The offset in the node.\n\t * @return {Object} The matching utterance.\n\t */\n\n\tthis.getStartUtterance = function ( node, offset ) {\n\t\tfor ( ; offset < node.textContent.length; offset++ ) {\n\t\t\tfor ( let i = 0; i < self.utterances.length; i++ ) {\n\t\t\t\tconst utterance = self.utterances[ i ];\n\t\t\t\tif (\n\t\t\t\t\tself.isPointInItems(\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\tutterance.content,\n\t\t\t\t\t\toffset,\n\t\t\t\t\t\tutterance.startOffset,\n\t\t\t\t\t\tutterance.endOffset\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\treturn utterance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// No match found in the given node, check the next one.\n\t\tconst nextTextNode = self.getNextTextNode( node );\n\t\treturn self.getStartUtterance( nextTextNode, 0 );\n\t};\n\n\t/**\n\t * Check if a point in the text is in any of a number of items.\n\t *\n\t * Checks if a node is present in any of the items. When a\n\t * matching item is found, checks if the offset falls between\n\t * the given min and max values.\n\t *\n\t * @param {Text} node The node to check.\n\t * @param {Object[]} items Item objects containing a path to\n\t * the node they were created from.\n\t * @param {number} offset Offset in the node.\n\t * @param {number} minOffset The minimum offset to be\n\t * considered a match.\n\t * @param {number} maxOffset The maximum offset to be\n\t * considered a match.\n\t */\n\n\tthis.isPointInItems = function (\n\t\tnode,\n\t\titems,\n\t\toffset,\n\t\tminOffset,\n\t\tmaxOffset\n\t) {\n\t\tif ( items.length === 1 ) {\n\t\t\tconst item = items[ 0 ];\n\t\t\tif (\n\t\t\t\tself.getNodeForItem( item ) === node &&\n\t\t\t\t\toffset >= minOffset &&\n\t\t\t\t\toffset <= maxOffset\n\t\t\t) {\n\t\t\t\t// Just check if the offset is within the min and\n\t\t\t\t// max offsets, if there is only one item.\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( let i = 0; i < items.length; i++ ) {\n\t\t\t\tconst item = items[ i ];\n\t\t\t\tif ( self.getNodeForItem( item ) !== node ) {\n\t\t\t\t\t// Skip items that don't match the node we're\n\t\t\t\t\t// looking for.\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst index = items.indexOf( item );\n\t\t\t\tif ( index === 0 ) {\n\t\t\t\t\tif ( offset >= minOffset ) {\n\t\t\t\t\t\t// For the first node, check if position is\n\t\t\t\t\t\t// after the start of the utterance.\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t} else if ( index === items.length - 1 ) {\n\t\t\t\t\tif ( offset <= maxOffset ) {\n\t\t\t\t\t\t// For the last node, check if position is\n\t\t\t\t\t\t// before end of utterance.\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Any other node should be entirely within the\n\t\t\t\t\t// utterance.\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t};\n\n\t/**\n\t * Get the first text node after a given node.\n\t *\n\t * @param {HTMLElement|Text} node Get the text node after\n\t * this one.\n\t * @return {Text} The first node after `node`.\n\t */\n\n\tthis.getNextTextNode = function ( node ) {\n\t\tconst nextNode = node.nextSibling;\n\t\tif ( nextNode === null ) {\n\t\t\t// No more text nodes, start traversing the DOM\n\t\t\t// upward, checking sibling of ancestors.\n\t\t\treturn self.getNextTextNode( node.parentNode );\n\t\t} else if ( nextNode.nodeType === 1 ) {\n\t\t\t// Node is an element, find the first text node in\n\t\t\t// it's children.\n\t\t\tfor ( let i = 0; i < nextNode.childNodes.length; i++ ) {\n\t\t\t\tconst child = nextNode.childNodes[ i ];\n\t\t\t\tconst textNode = self.getFirstTextNode( child );\n\t\t\t\tif ( textNode ) {\n\t\t\t\t\treturn textNode;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn self.getNextTextNode( nextNode );\n\t\t} else if ( nextNode.nodeType === 3 ) {\n\t\t\treturn nextNode;\n\t\t}\n\t};\n\n\t/**\n\t * Get the token containing a point, searching forward.\n\t *\n\t * Finds the token that contains a point in the text,\n\t * specified by a node and an offset in that node. Several\n\t * tokens may contain parts of the same node, which is why\n\t * the offset is needed.\n\t *\n\t * If the offset can't be found in the given node, later nodes\n\t * are checked. This happens if the offset falls between two\n\t * tokens.\n\t *\n\t * @param {Object} utterance The utterance to look for tokens in.\n\t * @param {Text} node The node that contains the token.\n\t * @param {number} offset The offset in the node.\n\t * @param {Object} The first token found.\n\t */\n\n\tthis.getStartToken = function ( utterance, node, offset ) {\n\t\tfor ( ; offset < node.textContent.length; offset++ ) {\n\t\t\tfor ( let i = 0; i < utterance.tokens.length; i++ ) {\n\t\t\t\tconst token = utterance.tokens[ i ];\n\t\t\t\tif (\n\t\t\t\t\tself.isPointInItems(\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\ttoken.items,\n\t\t\t\t\t\toffset,\n\t\t\t\t\t\ttoken.startOffset,\n\t\t\t\t\t\ttoken.endOffset\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\treturn token;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// If token wasn't found in the given node, check the next\n\t\t// one.\n\t\tconst nextTextNode = self.getNextTextNode( node );\n\t\treturn self.getStartToken( utterance, nextTextNode, 0 );\n\t};\n\n\t/**\n\t * Get the last text node that is a descendant of given node.\n\t *\n\t * Finds the depth first text node, i.e. in\n\t * `<a>1<b>2</b></a>`\n\t * the node with text \"2\" is the last one. If the given node\n\t * is itself a text node, it is simply returned.\n\t *\n\t * @param {HTMLElement} node The node under which to look for\n\t * text nodes.\n\t * @param {boolean} inUtterance If true, the last text node\n\t * that is also in an utterance is returned.\n\t * @return {Text} The last text node under `node`,\n\t * undefined if there are no text nodes.\n\t */\n\n\tthis.getLastTextNode = function ( node, inUtterance ) {\n\t\tif ( node.nodeType === 3 ) {\n\t\t\tif ( !inUtterance || self.isNodeInUtterance( node ) ) {\n\t\t\t\t// The given node is a text node. Check whether\n\t\t\t\t// the node is in an utterance, if that is\n\t\t\t\t// requested.\n\t\t\t\treturn node;\n\t\t\t}\n\t\t} else {\n\t\t\tfor ( let i = node.childNodes.length - 1; i >= 0; i-- ) {\n\t\t\t\t// Check children if the given node is an element.\n\t\t\t\tconst child = node.childNodes[ i ];\n\t\t\t\tconst textNode = self.getLastTextNode( child, inUtterance );\n\t\t\t\tif ( textNode ) {\n\t\t\t\t\treturn textNode;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Get the utterance containing a point, searching backward.\n\t *\n\t * Finds the utterance that contains a point in the text,\n\t * specified by a node and an offset in that node. Several\n\t * utterances may contain parts of the same node, which is why\n\t * the offset is needed.\n\t *\n\t * If the offset can't be found in the given node, preceding\n\t * nodes are checked. This happens if the offset falls between\n\t * two utterances.\n\t *\n\t * @param {Text} node The first node to check.\n\t * @param {number} offset The offset in the node.\n\t * @return {Object} The matching utterance.\n\t */\n\n\tthis.getEndUtterance = function ( node, offset ) {\n\t\tfor ( ; offset >= 0; offset-- ) {\n\t\t\tfor ( let i = 0; i < self.utterances.length; i++ ) {\n\t\t\t\tconst utterance = self.utterances[ i ];\n\t\t\t\tif (\n\t\t\t\t\tself.isPointInItems(\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\tutterance.content,\n\t\t\t\t\t\toffset,\n\t\t\t\t\t\tutterance.startOffset,\n\t\t\t\t\t\tutterance.endOffset\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\treturn utterance;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst previousTextNode = self.getPreviousTextNode( node );\n\t\treturn self.getEndUtterance(\n\t\t\tpreviousTextNode,\n\t\t\tpreviousTextNode.textContent.length\n\t\t);\n\t};\n\n\t/**\n\t * Get the first text node before a given node.\n\t *\n\t * @param {HTMLElement|Text} node Get the text node before\n\t * this one.\n\t * @return {Text} The first node before `node`.\n\t */\n\n\tthis.getPreviousTextNode = function ( node ) {\n\t\tconst previousNode = node.previousSibling;\n\t\tif ( previousNode === null ) {\n\t\t\treturn self.getPreviousTextNode( node.parentNode );\n\t\t} else if ( previousNode.nodeType === 1 ) {\n\t\t\tfor ( let i = previousNode.childNodes.length - 1; i >= 0; i-- ) {\n\t\t\t\tconst child = previousNode.childNodes[ i ];\n\t\t\t\tconst textNode = self.getLastTextNode( child );\n\t\t\t\tif ( textNode ) {\n\t\t\t\t\treturn textNode;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn self.getPreviousTextNode( previousNode );\n\t\t} else if ( previousNode.nodeType === 3 ) {\n\t\t\treturn previousNode;\n\t\t}\n\t};\n\n\t/**\n\t * Get the token containing a point, searching backward.\n\t *\n\t * Finds the token that contains a point in the text,\n\t * specified by a node and an offset in that node. Several\n\t * tokens may contain parts of the same node, which is why\n\t * the offset is needed.\n\t *\n\t * If the offset can't be found in the given node, preceding\n\t * nodes are checked. This happens if the offset falls between\n\t * two tokens.\n\t *\n\t * @param {Object} utterance The utterance to look for tokens in.\n\t * @param {Text} node The node that contains the token.\n\t * @param {number} offset The offset in the node.\n\t * @param {Object} The first token found.\n\t */\n\n\tthis.getEndToken = function ( utterance, node, offset ) {\n\t\tfor ( ; offset >= 0; offset-- ) {\n\t\t\tfor ( let i = 0; i < utterance.tokens.length; i++ ) {\n\t\t\t\tconst token = utterance.tokens[ i ];\n\t\t\t\tif (\n\t\t\t\t\tself.isPointInItems(\n\t\t\t\t\t\tnode,\n\t\t\t\t\t\ttoken.items,\n\t\t\t\t\t\toffset,\n\t\t\t\t\t\ttoken.startOffset,\n\t\t\t\t\t\ttoken.endOffset\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\treturn token;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst previousTextNode = self.getPreviousTextNode( node );\n\t\treturn self.getEndToken(\n\t\t\tutterance,\n\t\t\tpreviousTextNode,\n\t\t\tpreviousTextNode.textContent.length\n\t\t);\n\t};\n\n\t/**\n\t * Find the text node from which a content item was created.\n\t *\n\t * The path property of the item is an XPath expression\n\t * that is used to traverse the DOM tree.\n\t *\n\t * @param {Object} item The item to find the text node for.\n\t * @return {Text} The text node associated with the item.\n\t */\n\n\tthis.getNodeForItem = function ( item ) {\n\t\t// The path should be unambiguous, so just get the first\n\t\t// matching node.\n\t\tconst contentSelector = mw.config.get( 'wgWikispeechContentSelector' );\n\t\tconst result = document.evaluate(\n\t\t\titem.path,\n\t\t\t$( contentSelector ).get( 0 ),\n\t\t\tnull,\n\t\t\tXPathResult.FIRST_ORDERED_NODE_TYPE,\n\t\t\tnull\n\t\t);\n\t\tconst node = result.singleNodeValue;\n\t\treturn node;\n\t};\n}\n\nmw.wikispeech = mw.wikispeech || {};\nmw.wikispeech.Storage = Storage;\nmw.wikispeech.storage = new Storage();\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.transcriptionPreviewer.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":64,"column":18,"nodeType":"CallExpression","endLine":72,"endColumn":5},{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .fail","line":64,"column":18,"nodeType":"CallExpression","endLine":78,"endColumn":5}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"const util = require( './ext.wikispeech.util.js' );\n\n/**\n * Generates audio preview for the transcription in SpecialEditLexicon.\n *\n * @class TranscriptionPreviewer\n * @param {jQuery} $language\n * @param {jQuery} $transcription\n * @param {mw.Api} api\n * @param {jQuery} $player\n */\nfunction TranscriptionPreviewer(\n\t$language,\n\t$transcription,\n\tapi,\n\t$player\n) {\n\tthis.$language = $language;\n\tthis.$transcription = $transcription;\n\tthis.api = api;\n\tthis.$player = $player;\n}\n\n/**\n * Play the transcription using TTS.\n *\n * If the transcription has changed since last play, a new one\n * retrieved. Otherwise the previous one is replayed.\n *\n * @return {jQuery.Promise} Fulfilled when audio starts playing, rejected if\n * audio was not present.\n */\nTranscriptionPreviewer.prototype.play = function () {\n\tconst transcription = this.$transcription.val();\n\t// Rewind in case it is already playing. Just calling play() is not enought to play from start.\n\tthis.$player.prop( 'currentTime', 0 );\n\tconst self = this;\n\tlet promise;\n\tif ( transcription !== this.lastTranscription || !this.$player.attr( 'src' ) ) {\n\t\tpromise = this.fetchAudio().then( () => {\n\t\t\tself.$player.get( 0 ).play();\n\t\t} );\n\t\tthis.lastTranscription = transcription;\n\t} else {\n\t\tthis.$player.get( 0 ).play();\n\t\tpromise = $.Deferred().resolve().promise();\n\t}\n\n\treturn promise;\n};\n\n/**\n * Get audio for the player using the listen API\n *\n * @return {jQuery.Promise} Fulfilled when audio is fetched, rejected\n * if there was an error.\n */\nTranscriptionPreviewer.prototype.fetchAudio = function () {\n\tconst language = this.$language.val();\n\tconst voice = util.getUserVoice( language );\n\tconst transcription = this.$transcription.val();\n\tmw.log( 'Fetching transcription preview for (' + language + '): ' + transcription );\n\tconst self = this;\n\tconst request = this.api.get( {\n\t\taction: 'wikispeech-listen',\n\t\tlang: language,\n\t\tipa: transcription,\n\t\tvoice: voice\n\t} ).done( ( response ) => {\n\t\tconst audioData = response[ 'wikispeech-listen' ].audio;\n\t\tself.$player.attr( 'src', 'data:audio/ogg;base64,' + audioData );\n\t} ).fail( ( code, result ) => {\n\t\tself.$player.attr( 'src', '' );\n\t\tmw.log.error( 'Failed to synthesize:', code, result );\n\t\tconst message = result.error.info;\n\t\tconst title = mw.msg( 'wikispeech-error-generate-preview-title' );\n\t\tOO.ui.alert( message, { title: title, size: 'medium' } );\n\t} );\n\n\treturn request;\n};\n\nmodule.exports = TranscriptionPreviewer;\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.ui.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":94,"column":3,"nodeType":"CallExpression","endLine":102,"endColumn":7}],"suppressedMessages":[{"ruleId":"mediawiki/class-doc","severity":2,"message":"All possible CSS classes should be documented. See https://w.wiki/PS2 for details.","line":178,"column":42,"nodeType":"ObjectExpression","endLine":182,"endColumn":4,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Creates and controls the UI for the extension.\n *\n * @class ext.wikispeech.Ui\n * @constructor\n */\n\nfunction Ui() {\n\tconst self = this;\n\t// Resolves the UI is ready to be extended by consumer.\n\tself.ready = $.Deferred();\n\n\t/**\n\t * Initialize elements and functionality for the UI.\n\t */\n\n\tthis.init = function () {\n\t\tself.addSelectionPlayer();\n\t\tself.addControlPanel();\n\t\tself.addKeyboardShortcuts();\n\t\tself.windowManager = new OO.ui.WindowManager();\n\t\tself.addDialogs();\n\t\tself.ready.resolve();\n\t};\n\n\t/**\n\t * Add a panel with controls for for Wikispeech.\n\t *\n\t * The panel contains buttons for controlling playback and\n\t * links to related pages.\n\t */\n\n\tthis.addControlPanel = function () {\n\t\tconst toolFactory = new OO.ui.ToolFactory();\n\t\tconst toolGroupFactory = new OO.ui.ToolGroupFactory();\n\t\tself.toolbar = new OO.ui.Toolbar(\n\t\t\ttoolFactory,\n\t\t\ttoolGroupFactory,\n\t\t\t{\n\t\t\t\tactions: true,\n\t\t\t\tclasses: [ 'ext-wikispeech-control-panel' ],\n\t\t\t\tposition: 'bottom'\n\t\t\t}\n\t\t);\n\n\t\tconst playerGroup = self.addToolbarGroup();\n\t\tself.addButton(\n\t\t\tplayerGroup,\n\t\t\t'first',\n\t\t\tmw.wikispeech.player.skipBackUtterance,\n\t\t\tmw.msg( 'wikispeech-skip-back' )\n\t\t);\n\t\tself.addButton(\n\t\t\tplayerGroup,\n\t\t\t'previous',\n\t\t\tmw.wikispeech.player.skipBackToken,\n\t\t\tmw.msg( 'wikispeech-previous' )\n\t\t);\n\t\tself.playStopButton = self.addButton(\n\t\t\tplayerGroup,\n\t\t\t'play',\n\t\t\tmw.wikispeech.player.playOrStop,\n\t\t\tmw.msg( 'wikispeech-play' ),\n\t\t\t[ 'ext-wikispeech-play-stop' ]\n\t\t);\n\t\tself.addButton(\n\t\t\tplayerGroup,\n\t\t\t'next',\n\t\t\tmw.wikispeech.player.skipAheadToken,\n\t\t\tmw.msg( 'wikispeech-next' )\n\t\t);\n\t\tself.addButton(\n\t\t\tplayerGroup,\n\t\t\t'last',\n\t\t\tmw.wikispeech.player.skipAheadUtterance,\n\t\t\tmw.msg( 'wikispeech-skip-ahead' )\n\t\t);\n\n\t\tself.linkGroup = self.addToolbarGroup();\n\t\tself.addLinkConfigButton(\n\t\t\tself.linkGroup,\n\t\t\t'help',\n\t\t\t'wgWikispeechHelpPage',\n\t\t\tmw.msg( 'wikispeech-help' )\n\t\t);\n\t\tself.addLinkConfigButton(\n\t\t\tself.linkGroup,\n\t\t\t'feedback',\n\t\t\t'wgWikispeechFeedbackPage',\n\t\t\tmw.msg( 'wikispeech-feedback' )\n\n\t\t);\n\t\tconst api = new mw.Api();\n\t\tapi.getUserInfo()\n\t\t\t.done( ( info ) => {\n\t\t\t\tconst canEditLexicon = info.rights.includes( 'wikispeech-edit-lexicon' );\n\t\t\t\tif ( !canEditLexicon ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tself.addEditButton();\n\t\t\t} );\n\n\t\t$( document.body ).append( self.toolbar.$element );\n\t\tself.toolbar.initialize();\n\n\t\t// Add extra padding at the bottom of the page to not have\n\t\t// the player cover anything.\n\t\tconst height = self.toolbar.$element.height();\n\t\tself.$playerFooter = $( '<div>' )\n\t\t\t.height( height )\n\t\t\t// A bit of CSS is needed to make it interact properly\n\t\t\t// with the other floating elements in the footer.\n\t\t\t.css( {\n\t\t\t\tfloat: 'left',\n\t\t\t\twidth: '100%'\n\t\t\t} )\n\t\t\t.appendTo( '#footer' );\n\t\tself.addBufferingIcon();\n\t};\n\n\t/**\n\t * Add button that takes the user to the lexicon editor.\n\t *\n\t * @param {string} If given, this is used to build the URL for\n\t * the editor page. It should be the URL to the script\n\t * endpoint of a wiki, i.e. \"...index.php\". If not given the\n\t * link will go to the page on the local wiki.\n\t */\n\n\tthis.addEditButton = function ( scriptUrl ) {\n\t\tlet editUrl;\n\t\tif ( scriptUrl ) {\n\t\t\teditUrl = scriptUrl;\n\t\t} else {\n\t\t\teditUrl = mw.config.get( 'wgScript' );\n\t\t}\n\t\teditUrl += '?' + new URLSearchParams( {\n\t\t\ttitle: 'Special:EditLexicon',\n\t\t\tlanguage: mw.config.get( 'wgPageContentLanguage' ),\n\t\t\tpage: mw.config.get( 'wgArticleId' )\n\t\t} );\n\t\tself.addButton(\n\t\t\tself.linkGroup,\n\t\t\t'edit',\n\t\t\teditUrl,\n\t\t\tmw.msg( 'wikispeech-edit-lexicon-btn' ),\n\t\t\tnull,\n\t\t\t'wikispeech-edit'\n\t\t);\n\t};\n\n\t/**\n\t * Add a group to the player toolbar.\n\t *\n\t * @return {OO.ui.ButtonGroupWidget}\n\t */\n\n\tthis.addToolbarGroup = function () {\n\t\tconst group = new OO.ui.ButtonGroupWidget();\n\t\tself.toolbar.$actions.append( group.$element );\n\t\treturn group;\n\t};\n\n\t/**\n\t * Add a control button.\n\t *\n\t * @param {OO.ui.ButtonGroupWidget} group Group to add button to.\n\t * @param {string} icon Name of button icon.\n\t * @param {Function|string} onClick Function to call or link.\n\t * @param {string[]} classes Classes to add to the button.\n\t * @param {string} id Id to add to the button.\n\t * @return {OO.ui.ButtonWidget}\n\t */\n\n\tthis.addButton = function ( group, icon, onClick, ariaLabel, classes, id ) {\n\t\t// eslint-disable-next-line mediawiki/class-doc\n\t\tconst button = new OO.ui.ButtonWidget( {\n\t\t\ticon: icon,\n\t\t\tclasses: classes,\n\t\t\tid: id\n\t\t} );\n\t\tif ( typeof onClick === 'function' ) {\n\t\t\tbutton.on( 'click', onClick );\n\t\t} else if ( typeof onClick === 'string' ) {\n\t\t\tbutton.setHref( onClick );\n\t\t\t// Open link in new tab or window.\n\t\t\tbutton.setTarget( '_blank' );\n\t\t}\n\t\tif ( ariaLabel ) {\n\t\t\tbutton.$element.find( 'a' ).attr( 'aria-label', ariaLabel );\n\t\t}\n\t\tgroup.addItems( [ button ] );\n\t\treturn button;\n\t};\n\n\t/**\n\t * Add buffering icon to the play/stop button.\n\t *\n\t * The icon shows when the waiting for audio to play.\n\t */\n\n\tthis.addBufferingIcon = function () {\n\t\tconst $playStopButtons = $(\n\t\t\tself.toolbar.$element\n\t\t\t\t.find( '.ext-wikispeech-play-stop' )\n\t\t)\n\t\t\t.add( self.selectionPlayer.$element );\n\t\tconst $containers = $( '<span>' )\n\t\t\t.addClass( 'ext-wikispeech-buffering-icon-container' )\n\t\t\t.appendTo( ( $playStopButtons ).find( '.oo-ui-iconElement-icon' ) );\n\t\tself.$bufferingIcons = $( '<span>' )\n\t\t\t.addClass( 'ext-wikispeech-buffering-icon' )\n\t\t\t.appendTo( $containers )\n\t\t\t.hide();\n\t};\n\n\t/**\n\t * Hide the buffering icon.\n\t */\n\n\tthis.hideBufferingIcon = function () {\n\t\tself.$bufferingIcons.hide();\n\t};\n\n\t/**\n\t * Show the buffering icon if the current audio is loading.\n\t */\n\n\tthis.showBufferingIconIfAudioIsLoading = function ( audio ) {\n\t\tif ( self.audioIsReady( audio ) ) {\n\t\t\tself.hideBufferingIcon();\n\t\t} else {\n\t\t\t$( audio ).on( 'canplay', () => {\n\t\t\t\tself.hideBufferingIcon();\n\t\t\t} );\n\t\t\tself.$bufferingIcons.show();\n\t\t}\n\t};\n\n\t/**\n\t * Check if the current audio is ready to play.\n\t *\n\t * The audio is deemed ready to play as soon as any playable\n\t * data is available.\n\t *\n\t * @param {HTMLElement} audio The audio element to test.\n\t * @return {boolean} True if the audio is ready to play else false.\n\t */\n\n\tthis.audioIsReady = function ( audio ) {\n\t\treturn audio.readyState >= 2;\n\t};\n\n\t/**\n\t * Remove canplay listener for the audio to hide buffering icon.\n\t *\n\t * @param {jQuery} $audioElement Audio element from which the\n\t * listener is removed.\n\t */\n\n\tthis.removeCanPlayListener = function ( $audioElement ) {\n\t\t$audioElement.off( 'canplay' );\n\t};\n\n\t/**\n\t * Change the icon of the play/stop button to stop.\n\t */\n\n\tthis.setPlayStopIconToStop = function () {\n\t\tself.playStopButton.setIcon( 'stop' );\n\t\tself.selectionPlayer.setIcon( 'stop' );\n\t};\n\n\t/**\n\t * Change the icon of the play/stop button to play.\n\t */\n\n\tthis.setPlayStopIconToPlay = function () {\n\t\tself.playStopButton.setIcon( 'play' );\n\t\tself.selectionPlayer.setIcon( 'play' );\n\t};\n\n\t/**\n\t * Add a button that takes the user to another page.\n\t *\n\t * The button gets the link destination from a supplied\n\t * config variable. If the variable isn't specified, the button\n\t * isn't added.\n\t *\n\t * @param {OO.ui.ButtonGroupWidget} group Group to add button to.\n\t * @param {string} icon Name of button icon.\n\t * @param {string} configVariable The config variable to get\n\t * link destination from.\n\t */\n\n\tthis.addLinkConfigButton = function ( group, icon, configVariable, ariaLabel ) {\n\t\tconst url = mw.config.get( configVariable );\n\t\tif ( url ) {\n\t\t\tself.addButton( group, icon, url, ariaLabel );\n\t\t}\n\t};\n\n\t/**\n\t * Add a small player that appears when text is selected.\n\t */\n\n\tthis.addSelectionPlayer = function () {\n\t\tself.selectionPlayer = new OO.ui.ButtonWidget( {\n\t\t\ticon: 'play',\n\t\t\tclasses: [\n\t\t\t\t'ext-wikispeech-selection-player',\n\t\t\t\t'ext-wikispeech-play-stop'\n\t\t\t]\n\t\t} )\n\t\t\t.on( 'click', mw.wikispeech.player.playOrStop );\n\t\tself.selectionPlayer.toggle( false );\n\t\t$( document.body ).append( self.selectionPlayer.$element );\n\t\t$( document ).on( 'mouseup', () => {\n\t\t\tif (\n\t\t\t\tself.isShown() &&\n\t\t\t\tmw.wikispeech.selectionPlayer.isSelectionValid()\n\t\t\t) {\n\t\t\t\tself.showSelectionPlayer();\n\t\t\t} else {\n\t\t\t\tself.selectionPlayer.toggle( false );\n\t\t\t}\n\t\t} );\n\t\t$( document ).on( 'click', () => {\n\t\t\t// A click listener is also needed because of the\n\t\t\t// order of events when text is deselected by clicking\n\t\t\t// it.\n\t\t\tif ( !mw.wikispeech.selectionPlayer.isSelectionValid() ) {\n\t\t\t\tself.selectionPlayer.toggle( false );\n\t\t\t}\n\t\t} );\n\t};\n\n\t/**\n\t * Check if control panel is shown\n\t *\n\t * @return {boolean} Visibility of control panel.\n\t */\n\n\tthis.isShown = function () {\n\t\treturn self.toolbar.isVisible();\n\t};\n\n\t/**\n\t * Show the selection player below the end of the selection.\n\t */\n\n\tthis.showSelectionPlayer = function () {\n\n\t\tself.selectionPlayer.toggle( true );\n\t\tconst selection = window.getSelection();\n\t\tconst lastRange = selection.getRangeAt( selection.rangeCount - 1 );\n\t\tconst lastRect =\n\t\t\tmw.wikispeech.util.getLast( lastRange.getClientRects() );\n\t\t// Place the player under the end of the selected text.\n\t\tlet left;\n\t\tif ( self.getTextDirection( lastRange.endContainer ) === 'rtl' ) {\n\t\t\t// For RTL languages, the end of the text is the far left.\n\t\t\tleft = lastRect.left + $( document ).scrollLeft();\n\t\t} else {\n\t\t\t// For LTR languages, the end of the text is the far\n\t\t\t// right. This is the default value for the direction\n\t\t\t// property.\n\t\t\tleft =\n\t\t\t\tlastRect.right +\n\t\t\t\t$( document ).scrollLeft() -\n\t\t\t\tself.selectionPlayer.$element.width();\n\t\t}\n\t\tconst top = lastRect.bottom + $( document ).scrollTop();\n\t\tself.selectionPlayer.$element.css( {\n\t\t\tleft: left + 'px',\n\t\t\ttop: top + 'px'\n\t\t} );\n\t};\n\n\t/**\n\t * Get the text direction for a node.\n\t *\n\t * @return {string} The CSS value of the `direction` property\n\t * for the node, or for its parent if it is a text node.\n\t */\n\n\tthis.getTextDirection = function ( node ) {\n\t\tif ( node.nodeType === 3 ) {\n\t\t\t// For text nodes, get the property of the parent element.\n\t\t\treturn $( node ).parent().css( 'direction' );\n\t\t} else {\n\t\t\treturn $( node ).css( 'direction' );\n\t\t}\n\t};\n\n\t/**\n\t * Register listeners for keyboard shortcuts.\n\t */\n\n\tthis.addKeyboardShortcuts = function () {\n\t\tconst shortcuts = mw.config.get( 'wgWikispeechKeyboardShortcuts' );\n\t\t$( document ).on( 'keydown', ( event ) => {\n\t\t\tif ( self.eventMatchShortcut( event, shortcuts.playStop ) ) {\n\t\t\t\tmw.wikispeech.player.playOrStop();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tself.eventMatchShortcut(\n\t\t\t\t\tevent,\n\t\t\t\t\tshortcuts.skipAheadSentence\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tmw.wikispeech.player.skipAheadUtterance();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tself.eventMatchShortcut(\n\t\t\t\t\tevent,\n\t\t\t\t\tshortcuts.skipBackSentence\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tmw.wikispeech.player.skipBackUtterance();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tself.eventMatchShortcut( event, shortcuts.skipAheadWord )\n\t\t\t) {\n\t\t\t\tmw.wikispeech.player.skipAheadToken();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tself.eventMatchShortcut( event, shortcuts.skipBackWord )\n\t\t\t) {\n\t\t\t\tmw.wikispeech.player.skipBackToken();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} );\n\t\t// Prevent keyup events from triggering if there is\n\t\t// keydown event for the same key combination. This caused\n\t\t// buttons in focus to trigger if a shortcut had space as\n\t\t// key.\n\t\t$( document ).on( 'keyup', ( event ) => {\n\t\t\tfor ( const name in shortcuts ) {\n\t\t\t\tconst shortcut = shortcuts[ name ];\n\t\t\t\tif ( self.eventMatchShortcut( event, shortcut ) ) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t} );\n\t};\n\n\t/**\n\t * Check if a keydown event matches a shortcut from the\n\t * configuration.\n\t *\n\t * Compare the key and modifier state (of ctrl, alt and shift)\n\t * for an event, to those of a shortcut from the\n\t * configuration.\n\t *\n\t * @param {Event} event The event to compare.\n\t * @param {Object} shortcut The shortcut object from the\n\t * config to compare to.\n\t * @return {boolean} true if key and all the modifiers match\n\t * with the shortcut, else false.\n\t */\n\n\tthis.eventMatchShortcut = function ( event, shortcut ) {\n\t\treturn event.which === shortcut.key &&\n\t\t\tevent.ctrlKey === shortcut.modifiers.includes( 'ctrl' ) &&\n\t\t\tevent.altKey === shortcut.modifiers.includes( 'alt' ) &&\n\t\t\tevent.shiftKey === shortcut.modifiers.includes( 'shift' );\n\t};\n\n\t/**\n\t * Create dialogs and add them to a window manager\n\t */\n\n\tthis.addDialogs = function () {\n\t\t$( document.body ).append( self.windowManager.$element );\n\t\tself.messageDialog = new OO.ui.MessageDialog();\n\t\tself.errorLoadAudioDialogData = {\n\t\t\ttitle: mw.msg( 'wikispeech-error-loading-audio-title' ),\n\t\t\tmessage: mw.msg( 'wikispeech-error-loading-audio-message' ),\n\t\t\tactions: [\n\t\t\t\t{\n\t\t\t\t\taction: 'retry',\n\t\t\t\t\tlabel: mw.msg( 'wikispeech-retry' ),\n\t\t\t\t\tflags: 'primary'\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\taction: 'stop',\n\t\t\t\t\tlabel: mw.msg( 'wikispeech-stop' ),\n\t\t\t\t\tflags: 'destructive'\n\t\t\t\t}\n\t\t\t]\n\t\t};\n\t\tself.addWindow( self.messageDialog );\n\t};\n\n\t/**\n\t * Add a window to the window manager.\n\t *\n\t * @param {OO.ui.Window} window\n\t */\n\n\tthis.addWindow = function ( window ) {\n\t\tself.windowManager.addWindows( [ window ] );\n\t};\n\n\t/**\n\t * Toggle GUI visibility\n\t *\n\t * Hides or shows control panel which also dictates whether\n\t * the selection player should be shown.\n\t */\n\n\tthis.toggleVisibility = function () {\n\t\tif ( self.isShown() ) {\n\t\t\tself.toolbar.toggle( false );\n\t\t\tself.selectionPlayer.toggle( false );\n\t\t\tself.$playerFooter.hide();\n\t\t} else {\n\t\t\tself.toolbar.toggle( true );\n\t\t\tself.selectionPlayer.toggle( true );\n\t\t\tself.$playerFooter.show();\n\t\t}\n\t};\n\n\t/**\n\t * Show an error dialog for when audio could not be loaded\n\t *\n\t * Has buttons for retrying and stopping playback.\n\t *\n\t * @return {jQuery.Promise} Resolves when dialog is closed.\n\t */\n\n\tthis.showLoadAudioError = function () {\n\t\treturn self.openWindow(\n\t\t\tself.messageDialog,\n\t\t\tself.errorLoadAudioDialogData\n\t\t);\n\t};\n\n\t/**\n\t * Open a window.\n\t *\n\t * @param {OO.ui.Window} window\n\t * @param {Object} data\n\t * @return {jQuery.Promise} Resolves when window is closed.\n\t */\n\n\tthis.openWindow = function ( window, data ) {\n\t\treturn self.windowManager.openWindow( window, data ).closed;\n\t};\n}\n\nmw.wikispeech = mw.wikispeech || {};\nmw.wikispeech.Ui = Ui;\nmw.wikispeech.ui = new Ui();\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.userOptionsDialog.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":2,"message":"Prefer .then to .done","line":95,"column":2,"nodeType":"CallExpression","endLine":115,"endColumn":5}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Popup dialog for Wikispeech user options.\n *\n * Replaces the normal user options when running on\n * consumer wiki.\n *\n * @class ext.wikispeech.UserOptionsDialog\n */\n\nlet self;\nconst util = require( './ext.wikispeech.util.js' );\n\nfunction UserOptionsDialog( config ) {\n\tUserOptionsDialog.super.call( this, config );\n\tself = this;\n}\n\nOO.inheritClass( UserOptionsDialog, OO.ui.ProcessDialog );\nUserOptionsDialog.static.name = 'UserOptionsDialog';\nUserOptionsDialog.static.title = mw.msg( 'preferences' );\nUserOptionsDialog.static.actions = [\n\t{\n\t\taction: 'save',\n\t\tlabel: mw.msg( 'saveprefs' ),\n\t\tflags: [ 'primary', 'progressive' ]\n\t},\n\t{\n\t\tflags: [ 'safe', 'close' ]\n\t}\n];\n\nUserOptionsDialog.prototype.initialize = function () {\n\tUserOptionsDialog.super.prototype.initialize.apply( self );\n\tconst panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );\n\tconst content = new OO.ui.FieldsetLayout();\n\tconst voiceFieldset = self.addVoiceFieldset();\n\n\t// Add input field for speech rate, shown in percent.\n\tself.speechRateInput = new OO.ui.NumberInputWidget( {\n\t\tmin: 0,\n\t\tstep: 25,\n\t\tvalue: mw.user.options.get( 'wikispeechSpeechRate' ) * 100\n\t} );\n\tconst speechRateFieldset = new OO.ui.FieldsetLayout( {\n\t\tlabel: mw.msg( 'prefs-wikispeech-speech-rate-percent' )\n\t} );\n\tconst speechRateField = new OO.ui.FieldLayout( self.speechRateInput );\n\tspeechRateFieldset.addItems( [ speechRateField ] );\n\n\t// Add a notice about needing to reload the page before\n\t// preferences kick in.\n\tconst notice = new OO.ui.MessageWidget( {\n\t\ttype: 'notice',\n\t\tlabel: mw.msg( 'wikispeech-notice-prefs-apply-on-next-page-load' )\n\t} );\n\tconst noticeFieldset = new OO.ui.FieldsetLayout();\n\tnoticeFieldset.addItems( [ new OO.ui.FieldLayout( notice ) ] );\n\n\tcontent.addItems( [\n\t\tvoiceFieldset,\n\t\tspeechRateFieldset,\n\t\tnoticeFieldset\n\t] );\n\tpanel.$element.append( content.$element );\n\tself.$body.append( panel.$element );\n};\n\n/**\n * Add fields for selecting voice.\n *\n * Adds two fields: language and voice. When a language is\n * selected, the voice is populated by the available voices for\n * that language. Language defaults to the language of the current\n * page. Voices are labeled with language code and autonym.\n *\n * @return {OO.ui.FieldsetLayout}\n */\n\nUserOptionsDialog.prototype.addVoiceFieldset = function () {\n\tconst voices = mw.config.get( 'wgWikispeechVoices' );\n\tconst languageItems = [];\n\tconst languageCodes = Object.keys( voices );\n\tlanguageCodes.sort();\n\tlanguageCodes.forEach( ( language ) => {\n\t\tlanguageItems.push(\n\t\t\tnew OO.ui.MenuOptionWidget( {\n\t\t\t\tdata: language,\n\t\t\t\tlabel: language\n\t\t\t} )\n\t\t);\n\t} );\n\t// Add autonyms to labels. Do this separately to not break if\n\t// the request fails. If it does, we still have the language\n\t// codes as labels.\n\tnew mw.Api().get( {\n\t\taction: 'query',\n\t\tformat: 'json',\n\t\tformatversion: 2,\n\t\tmeta: 'languageinfo',\n\t\tliprop: 'autonym',\n\t\tlicode: languageCodes\n\t} ).done( ( response ) => {\n\t\tconst info = response.query.languageinfo;\n\t\tObject.keys( info ).forEach( ( code ) => {\n\t\t\tlanguageItems.forEach( ( item ) => {\n\t\t\t\tif ( item.label === code ) {\n\t\t\t\t\titem.setLabel( code + ' - ' + info[ code ].autonym );\n\t\t\t\t}\n\t\t\t} );\n\t\t} );\n\t\t// Reselect the language to show the new label.\n\t\tself.languageSelect.getMenu().selectItemByData(\n\t\t\tmw.config.get( 'wgPageContentLanguage' )\n\t\t);\n\t} );\n\tself.languageSelect = new OO.ui.DropdownWidget( {\n\t\tmenu: {\n\t\t\titems: languageItems\n\t\t}\n\t} );\n\n\tself.voiceSelect = new OO.ui.DropdownWidget();\n\t// Update the voice items when language is selected.\n\tself.languageSelect.getMenu().on( 'select', ( item ) => {\n\t\tconst voiceItems = [\n\t\t\tnew OO.ui.MenuOptionWidget( {\n\t\t\t\tdata: '',\n\t\t\t\tlabel: mw.msg( 'default' )\n\t\t\t} )\n\t\t];\n\t\tconst language = item.data;\n\t\tvoices[ language ].forEach( ( voice ) => {\n\t\t\tvoiceItems.push(\n\t\t\t\tnew OO.ui.MenuOptionWidget( {\n\t\t\t\t\tdata: voice,\n\t\t\t\t\tlabel: voice\n\t\t\t\t} )\n\t\t\t);\n\t\t} );\n\t\tself.voiceSelect.getMenu().clearItems();\n\t\tself.voiceSelect.getMenu().addItems( voiceItems );\n\t\tconst currentVoice = util.getUserVoice( language );\n\t\tself.voiceSelect.getMenu().selectItemByData( currentVoice );\n\t} );\n\t// Select the language for the current page, since that is\n\t// probably the one the user is interested in.\n\tself.languageSelect.getMenu().selectItemByData(\n\t\tmw.config.get( 'wgPageContentLanguage' )\n\t);\n\n\tconst fieldset = new OO.ui.FieldsetLayout(\n\t\t{ label: mw.msg( 'prefs-wikispeech-voice' ) }\n\t);\n\tconst languageField = new OO.ui.FieldLayout(\n\t\tself.languageSelect,\n\t\t{ label: mw.msg( 'wikispeech-language' ) }\n\t);\n\tconst voiceField = new OO.ui.FieldLayout(\n\t\tself.voiceSelect,\n\t\t{ label: mw.msg( 'prefs-wikispeech-voice' ) }\n\t);\n\tfieldset.addItems( [ languageField, voiceField ] );\n\treturn fieldset;\n};\n\n/**\n * Handle actions.\n *\n * Closes the dialog when \"Save\" is clicked.\n *\n * @param {Object} action\n * @return {OO.ui.Process}\n */\n\nUserOptionsDialog.prototype.getActionProcess = function ( action ) {\n\tif ( action ) {\n\t\treturn new OO.ui.Process( () => {\n\t\t\tself.close( { action: action } );\n\t\t} );\n\t}\n\treturn UserOptionsDialog.super.prototype.getActionProcess.call( self, action );\n};\n\n/**\n * Get the selected language and voice.\n *\n * @return {Object}\n * @return {string} return.variable User option variable name.\n * @return {string} return.voice Name of voice.\n */\n\nUserOptionsDialog.prototype.getVoice = function () {\n\tconst language = self.languageSelect.getMenu().findSelectedItem().data;\n\tconst voiceVariable = util.getVoiceConfigVariable( language );\n\tconst voice = self.voiceSelect.getMenu().findSelectedItem().data;\n\treturn { variable: voiceVariable, voice: voice };\n};\n\n/**\n * Get the selected speech rate.\n *\n * @return {number} Speech rate as a decimal number, i.e. 100% =\n * 1.0.\n */\n\nUserOptionsDialog.prototype.getSpeechRate = function () {\n\treturn self.speechRateInput.value / 100;\n};\n\nmodule.exports = UserOptionsDialog;\n","usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/modules/ext.wikispeech.util.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/package-lock.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/package.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/sql/abstractSchemaChanges/patch-wikispeech_utterance-wsu_date_stored.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/sql/tables.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/.eslintrc.json","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"indent","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"no-extra-parens","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.highlighter.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.player.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.selectionPlayer.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.storage.test.js","messages":[],"suppressedMessages":[{"ruleId":"no-jquery/no-parse-html-literal","severity":2,"message":"Prefer DOM building to parsing HTML literals","line":40,"column":51,"nodeType":"CallExpression","endLine":40,"endColumn":71,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-parse-html-literal","severity":2,"message":"Prefer DOM building to parsing HTML literals","line":87,"column":51,"nodeType":"CallExpression","endLine":87,"endColumn":71,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-parse-html-literal","severity":2,"message":"Prefer DOM building to parsing HTML literals","line":115,"column":51,"nodeType":"CallExpression","endLine":115,"endColumn":74,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.test.util.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.transcriptionPreviewer.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/ext.wikispeech.ui.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]},{"filePath":"/src/repo/tests/qunit/index.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[{"ruleId":"max-len","replacedBy":[]},{"ruleId":"arrow-parens","replacedBy":[]},{"ruleId":"arrow-spacing","replacedBy":[]},{"ruleId":"lines-between-class-members","replacedBy":[]},{"ruleId":"no-new-require","replacedBy":[]},{"ruleId":"template-curly-spacing","replacedBy":[]},{"ruleId":"implicit-arrow-linebreak","replacedBy":[]},{"ruleId":"array-bracket-spacing","replacedBy":[]},{"ruleId":"block-spacing","replacedBy":[]},{"ruleId":"brace-style","replacedBy":[]},{"ruleId":"comma-dangle","replacedBy":[]},{"ruleId":"comma-spacing","replacedBy":[]},{"ruleId":"comma-style","replacedBy":[]},{"ruleId":"computed-property-spacing","replacedBy":[]},{"ruleId":"dot-location","replacedBy":[]},{"ruleId":"eol-last","replacedBy":[]},{"ruleId":"func-call-spacing","replacedBy":[]},{"ruleId":"indent","replacedBy":[]},{"ruleId":"key-spacing","replacedBy":[]},{"ruleId":"keyword-spacing","replacedBy":[]},{"ruleId":"linebreak-style","replacedBy":[]},{"ruleId":"max-statements-per-line","replacedBy":[]},{"ruleId":"new-parens","replacedBy":[]},{"ruleId":"no-floating-decimal","replacedBy":[]},{"ruleId":"no-multi-spaces","replacedBy":[]},{"ruleId":"no-multiple-empty-lines","replacedBy":[]},{"ruleId":"no-new-object","replacedBy":["no-object-constructor"]},{"ruleId":"no-tabs","replacedBy":[]},{"ruleId":"no-trailing-spaces","replacedBy":[]},{"ruleId":"no-whitespace-before-property","replacedBy":[]},{"ruleId":"object-curly-spacing","replacedBy":[]},{"ruleId":"operator-linebreak","replacedBy":[]},{"ruleId":"quote-props","replacedBy":[]},{"ruleId":"quotes","replacedBy":[]},{"ruleId":"semi","replacedBy":[]},{"ruleId":"semi-spacing","replacedBy":[]},{"ruleId":"semi-style","replacedBy":[]},{"ruleId":"space-before-blocks","replacedBy":[]},{"ruleId":"space-before-function-paren","replacedBy":[]},{"ruleId":"space-in-parens","replacedBy":[]},{"ruleId":"space-infix-ops","replacedBy":[]},{"ruleId":"space-unary-ops","replacedBy":[]},{"ruleId":"spaced-comment","replacedBy":[]},{"ruleId":"switch-colon-spacing","replacedBy":[]},{"ruleId":"wrap-iife","replacedBy":[]},{"ruleId":"no-extra-semi","replacedBy":[]},{"ruleId":"no-mixed-spaces-and-tabs","replacedBy":[]}]}]
--- end ---
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
Disabling eslint rule 'no-jquery/no-done-fail' (broken in modules/.eslintrc.json) on modules/.eslintrc.json
$ /usr/bin/npm ci
--- stderr ---
npm WARN deprecated @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead
npm WARN deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
--- stdout ---
added 442 packages, and audited 443 packages in 5s
92 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
--- end ---
$ /usr/bin/npm test
--- stdout ---
> test
> grunt test && npm run doc
Running "eslint:all" (eslint) task
/src/repo/dev/speechoid-docker-compose/docker-compose.yml
18:1 warning This line has a length of 116. Maximum allowed is 100 max-len
23:1 warning This line has a length of 117. Maximum allowed is 100 max-len
29:1 warning This line has a length of 118. Maximum allowed is 100 max-len
48:1 warning This line has a length of 119. Maximum allowed is 100 max-len
55:1 warning This line has a length of 117. Maximum allowed is 100 max-len
69:1 warning This line has a length of 107. Maximum allowed is 100 max-len
78:1 warning This line has a length of 127. Maximum allowed is 100 max-len
/src/repo/modules/ext.wikispeech.gadget.js
39:3 warning Prefer .then to .done no-jquery/no-done-fail
39:3 warning Prefer .then to .fail no-jquery/no-done-fail
83:2 warning Prefer .then to .done no-jquery/no-done-fail
107:1 warning The type 'ext' is undefined jsdoc/no-undefined-types
115:2 warning Prefer .then to .done no-jquery/no-done-fail
115:2 warning Prefer .then to .fail no-jquery/no-done-fail
144:3 warning Prefer .then to .done no-jquery/no-done-fail
157:3 warning Prefer .then to .done no-jquery/no-done-fail
174:1 warning Prefer .then to .done no-jquery/no-done-fail
193:2 warning Prefer .then to .done no-jquery/no-done-fail
204:3 warning Prefer .then to .done no-jquery/no-done-fail
204:3 warning Prefer .then to .fail no-jquery/no-done-fail
206:5 warning Prefer .then to .done no-jquery/no-done-fail
206:5 warning Prefer .then to .fail no-jquery/no-done-fail
208:7 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.highlighter.js
30:1 warning This line has a length of 108. Maximum allowed is 100 max-len
/src/repo/modules/ext.wikispeech.loader.js
10:2 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.main.js
83:1 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.player.js
54:3 warning Prefer .then to .done no-jquery/no-done-fail
99:3 warning Prefer .then to .done no-jquery/no-done-fail
99:3 warning Prefer .then to .fail no-jquery/no-done-fail
112:5 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.selectionPlayer.js
144:3 warning Prefer .then to .done no-jquery/no-done-fail
179:3 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.storage.js
41:3 warning Prefer .then to .done no-jquery/no-done-fail
153:10 warning Prefer .then to .done no-jquery/no-done-fail
197:19 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.transcriptionPreviewer.js
64:18 warning Prefer .then to .done no-jquery/no-done-fail
64:18 warning Prefer .then to .fail no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.ui.js
94:3 warning Prefer .then to .done no-jquery/no-done-fail
/src/repo/modules/ext.wikispeech.userOptionsDialog.js
95:2 warning Prefer .then to .done no-jquery/no-done-fail
✖ 38 problems (0 errors, 38 warnings)
Running "banana:Wikispeech" (banana) task
>> 2 message directories checked.
Running "stylelint:all" (stylelint) task
>> Linted 1 files without errors
Done.
> doc
> jsdoc -c jsdoc.json
--- end ---
$ /usr/bin/npm audit --json
--- stdout ---
{
"auditReportVersion": 2,
"vulnerabilities": {},
"metadata": {
"vulnerabilities": {
"info": 0,
"low": 0,
"moderate": 0,
"high": 0,
"critical": 0,
"total": 0
},
"dependencies": {
"prod": 1,
"dev": 442,
"optional": 0,
"peer": 1,
"peerOptional": 0,
"total": 442
}
}
}
--- end ---
$ package-lock-lint package-lock.json
--- stdout ---
Checking package-lock.json
--- end ---
build: Updating eslint-config-wikimedia to 0.29.1
The following rules are failing and were disabled:
* modules:
* no-jquery/no-done-fail
$ git add .
--- stdout ---
--- end ---
$ git commit -F /tmp/tmp7nxud5zo
--- stdout ---
[master 393bab8] build: Updating eslint-config-wikimedia to 0.29.1
7 files changed, 23 insertions(+), 19 deletions(-)
--- end ---
$ git format-patch HEAD~1 --stdout
--- stdout ---
From 393bab86aa69fdc7fd2a932fd9a81263a0433d5c Mon Sep 17 00:00:00 2001
From: libraryupgrader <tools.libraryupgrader@tools.wmflabs.org>
Date: Mon, 31 Mar 2025 08:23:25 +0000
Subject: [PATCH] build: Updating eslint-config-wikimedia to 0.29.1
The following rules are failing and were disabled:
* modules:
* no-jquery/no-done-fail
Change-Id: I1ff257bc8f82c6b24f48eb369940aab6f6a36551
---
modules/.eslintrc.json | 3 ++-
modules/ext.wikispeech.gadget.js | 2 +-
modules/ext.wikispeech.main.js | 2 +-
modules/ext.wikispeech.ui.js | 8 ++++----
package-lock.json | 19 +++++++++++--------
package.json | 2 +-
tests/qunit/ext.wikispeech.ui.test.js | 6 +++---
7 files changed, 23 insertions(+), 19 deletions(-)
diff --git a/modules/.eslintrc.json b/modules/.eslintrc.json
index 8a2ef64..54d1204 100644
--- a/modules/.eslintrc.json
+++ b/modules/.eslintrc.json
@@ -9,6 +9,7 @@
"commonjs": true
},
"rules": {
- "compat/compat": "warn"
+ "compat/compat": "warn",
+ "no-jquery/no-done-fail": "warn"
}
}
diff --git a/modules/ext.wikispeech.gadget.js b/modules/ext.wikispeech.gadget.js
index ffd5b0d..7826fe4 100644
--- a/modules/ext.wikispeech.gadget.js
+++ b/modules/ext.wikispeech.gadget.js
@@ -86,7 +86,7 @@ function addUserOptions() {
// Take the option value from user page if it is set,
// otherwise use default.
- if ( Object.keys( options ).indexOf( key ) >= 0 ) {
+ if ( Object.keys( options ).includes( key ) ) {
value = options[ key ];
} else {
value = defaultOptions[ key ];
diff --git a/modules/ext.wikispeech.main.js b/modules/ext.wikispeech.main.js
index 53aff02..1bea16d 100644
--- a/modules/ext.wikispeech.main.js
+++ b/modules/ext.wikispeech.main.js
@@ -75,7 +75,7 @@ function Main() {
this.enabledForNamespace = function () {
const validNamespaces = mw.config.get( 'wgWikispeechNamespaces' );
const namespace = mw.config.get( 'wgNamespaceNumber' );
- return validNamespaces.indexOf( namespace ) >= 0;
+ return validNamespaces.includes( namespace );
};
}
diff --git a/modules/ext.wikispeech.ui.js b/modules/ext.wikispeech.ui.js
index a42b098..4d6084a 100644
--- a/modules/ext.wikispeech.ui.js
+++ b/modules/ext.wikispeech.ui.js
@@ -93,7 +93,7 @@ function Ui() {
const api = new mw.Api();
api.getUserInfo()
.done( ( info ) => {
- const canEditLexicon = info.rights.indexOf( 'wikispeech-edit-lexicon' ) >= 0;
+ const canEditLexicon = info.rights.includes( 'wikispeech-edit-lexicon' );
if ( !canEditLexicon ) {
return;
}
@@ -463,9 +463,9 @@ function Ui() {
this.eventMatchShortcut = function ( event, shortcut ) {
return event.which === shortcut.key &&
- event.ctrlKey === shortcut.modifiers.indexOf( 'ctrl' ) >= 0 &&
- event.altKey === shortcut.modifiers.indexOf( 'alt' ) >= 0 &&
- event.shiftKey === shortcut.modifiers.indexOf( 'shift' ) >= 0;
+ event.ctrlKey === shortcut.modifiers.includes( 'ctrl' ) &&
+ event.altKey === shortcut.modifiers.includes( 'alt' ) &&
+ event.shiftKey === shortcut.modifiers.includes( 'shift' );
};
/**
diff --git a/package-lock.json b/package-lock.json
index 4bd9fb3..13efe39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"": {
"name": "wikispeech",
"devDependencies": {
- "eslint-config-wikimedia": "0.28.2",
+ "eslint-config-wikimedia": "0.29.1",
"grunt": "1.6.1",
"grunt-banana-checker": "0.13.0",
"grunt-eslint": "24.3.0",
@@ -1534,9 +1534,9 @@
}
},
"node_modules/eslint-config-wikimedia": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.28.2.tgz",
- "integrity": "sha512-5+rdnT7wH1gpKAO6tHYThg78eMhZMruJzvqku3Y5iaEY/A7kSKLFpA/vOj/snys9fKjDHC9BXmArQh+agkOoJQ==",
+ "version": "0.29.1",
+ "resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.29.1.tgz",
+ "integrity": "sha512-4dbL5o3hKGSvreyrGZWLPoTDLFubZ575IQOPhUaTcpbTsi0u05TBEMsOyYkthTaK21vsFQqhSYtxp/xU93BSdA==",
"dev": true,
"dependencies": {
"browserslist-config-wikimedia": "^0.7.0",
@@ -1549,13 +1549,16 @@
"eslint-plugin-mediawiki": "^0.7.0",
"eslint-plugin-mocha": "^10.4.3",
"eslint-plugin-n": "^17.7.0",
- "eslint-plugin-no-jquery": "^3.0.1",
+ "eslint-plugin-no-jquery": "^3.1.1",
"eslint-plugin-qunit": "^8.1.1",
"eslint-plugin-security": "^1.7.1",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-vue": "^9.26.0",
"eslint-plugin-wdio": "^8.24.12",
"eslint-plugin-yml": "^1.14.0"
+ },
+ "engines": {
+ "node": ">=18 <23"
}
},
"node_modules/eslint-plugin-compat": {
@@ -1753,9 +1756,9 @@
}
},
"node_modules/eslint-plugin-no-jquery": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-3.0.2.tgz",
- "integrity": "sha512-n/+6p6PFhWDNPVLJj1463hw4OTIRBbROGcbhmtOHTgw7yihSKzkwZiQ00EJTneyeR3jRiw5lpWSMCCBhtb8t2g==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-3.1.1.tgz",
+ "integrity": "sha512-LTLO3jH/Tjr1pmxCEqtV6qmt+OChv8La4fwgG470JRpgxyFF4NOzoC9CRy92GIWD3Yjl0qLEgPmD2FLQWcNEjg==",
"dev": true,
"peerDependencies": {
"eslint": ">=8.0.0"
diff --git a/package.json b/package.json
index 51a67f2..50ee957 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"doc": "jsdoc -c jsdoc.json"
},
"devDependencies": {
- "eslint-config-wikimedia": "0.28.2",
+ "eslint-config-wikimedia": "0.29.1",
"grunt": "1.6.1",
"grunt-banana-checker": "0.13.0",
"grunt-eslint": "24.3.0",
diff --git a/tests/qunit/ext.wikispeech.ui.test.js b/tests/qunit/ext.wikispeech.ui.test.js
index edc998f..74827f5 100644
--- a/tests/qunit/ext.wikispeech.ui.test.js
+++ b/tests/qunit/ext.wikispeech.ui.test.js
@@ -274,9 +274,9 @@ QUnit.test( 'addSelectionPlayer(): hide selection player initially', ( assert )
function createKeydownEvent( keyCode, modifiers ) {
const event = $.Event( 'keydown' );
event.which = keyCode;
- event.ctrlKey = modifiers.indexOf( 'c' ) >= 0;
- event.altKey = modifiers.indexOf( 'a' ) >= 0;
- event.shiftKey = modifiers.indexOf( 's' ) >= 0;
+ event.ctrlKey = modifiers.includes( 'c' );
+ event.altKey = modifiers.includes( 'a' );
+ event.shiftKey = modifiers.includes( 's' );
return event;
}
--
2.39.2
--- end ---