mediawiki/extensions/Wikispeech: main (log #2466573)

sourcepatches

This run took 89 seconds.

From 4d4eccb0eed56c9cfc2c1f7a1c50eae6555f680f Mon Sep 17 00:00:00 2001
From: libraryupgrader <tools.libraryupgrader@tools.wmflabs.org>
Date: Tue, 5 May 2026 05:22:47 +0000
Subject: [PATCH] build: Updating dependencies
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

composer:
* mediawiki/mediawiki-codesniffer: 50.0.0 → 51.0.0

npm:
* eslint-config-wikimedia: 0.32.3 → 0.32.4
* postcss: 8.5.6 → 8.5.14
  * https://github.com/advisories/GHSA-qx2v-qp2m-jg93

Change-Id: Ieefb410f4df31c8b02e7a829e67faac06cc4c03f
---
 composer.json                         |   2 +-
 modules/ext.wikispeech.highlighter.js |   2 +-
 package-lock.json                     | 426 +++++++++++++++-----------
 package.json                          |   2 +-
 4 files changed, 249 insertions(+), 183 deletions(-)

diff --git a/composer.json b/composer.json
index 9678a16..75ae153 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
 {
 	"require-dev": {
-		"mediawiki/mediawiki-codesniffer": "50.0.0",
+		"mediawiki/mediawiki-codesniffer": "51.0.0",
 		"mediawiki/mediawiki-phan-config": "0.20.0",
 		"mediawiki/minus-x": "2.0.1",
 		"php-parallel-lint/php-console-highlighter": "1.0.0",
diff --git a/modules/ext.wikispeech.highlighter.js b/modules/ext.wikispeech.highlighter.js
index 1ac3a14..f170c2a 100644
--- a/modules/ext.wikispeech.highlighter.js
+++ b/modules/ext.wikispeech.highlighter.js
@@ -64,7 +64,7 @@ class Highlighter {
 			return;
 		}
 		// Class name is documented above
-		// eslint-disable-next-line mediawiki/class-doc
+
 		const span = $( '<span>' )
 			.addClass( this.utteranceHighlightingClass )
 			.get( 0 );
diff --git a/package-lock.json b/package-lock.json
index 5ce30ea..62a6ea5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
 		"": {
 			"name": "wikispeech",
 			"devDependencies": {
-				"eslint-config-wikimedia": "0.32.3",
+				"eslint-config-wikimedia": "0.32.4",
 				"grunt": "1.6.2",
 				"grunt-banana-checker": "0.13.0",
 				"grunt-eslint": "24.3.0",
@@ -245,19 +245,32 @@
 			}
 		},
 		"node_modules/@es-joy/jsdoccomment": {
-			"version": "0.76.0",
-			"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.76.0.tgz",
-			"integrity": "sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==",
+			"version": "0.86.0",
+			"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz",
+			"integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==",
 			"dev": true,
 			"dependencies": {
 				"@types/estree": "^1.0.8",
-				"@typescript-eslint/types": "^8.46.0",
-				"comment-parser": "1.4.1",
-				"esquery": "^1.6.0",
-				"jsdoc-type-pratt-parser": "~6.10.0"
+				"@typescript-eslint/types": "^8.58.0",
+				"comment-parser": "1.4.6",
+				"esquery": "^1.7.0",
+				"jsdoc-type-pratt-parser": "~7.2.0"
 			},
 			"engines": {
-				"node": ">=20.11.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
+			}
+		},
+		"node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": {
+			"version": "8.59.2",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
+			"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
+			"dev": true,
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
 			}
 		},
 		"node_modules/@es-joy/resolve.exports": {
@@ -270,9 +283,9 @@
 			}
 		},
 		"node_modules/@eslint-community/eslint-utils": {
-			"version": "4.7.0",
-			"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
-			"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+			"version": "4.9.1",
+			"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+			"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
 			"dev": true,
 			"dependencies": {
 				"eslint-visitor-keys": "^3.4.3"
@@ -394,9 +407,9 @@
 			}
 		},
 		"node_modules/@mdn/browser-compat-data": {
-			"version": "5.7.6",
-			"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz",
-			"integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==",
+			"version": "6.1.5",
+			"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-6.1.5.tgz",
+			"integrity": "sha512-PzdZZzRhcXvKB0begee28n5lvwAcinGKYuLZOVxHAZm+n7y01ddEGfdS1ZXRuVcV+ndG6mSEAE8vgudom5UjYg==",
 			"dev": true
 		},
 		"node_modules/@nodelib/fs.scandir": {
@@ -593,20 +606,19 @@
 			"dev": true
 		},
 		"node_modules/@typescript-eslint/eslint-plugin": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
-			"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
+			"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
 			"dev": true,
 			"dependencies": {
-				"@eslint-community/regexpp": "^4.10.0",
-				"@typescript-eslint/scope-manager": "8.46.0",
-				"@typescript-eslint/type-utils": "8.46.0",
-				"@typescript-eslint/utils": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0",
-				"graphemer": "^1.4.0",
-				"ignore": "^7.0.0",
+				"@eslint-community/regexpp": "^4.12.2",
+				"@typescript-eslint/scope-manager": "8.54.0",
+				"@typescript-eslint/type-utils": "8.54.0",
+				"@typescript-eslint/utils": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0",
+				"ignore": "^7.0.5",
 				"natural-compare": "^1.4.0",
-				"ts-api-utils": "^2.1.0"
+				"ts-api-utils": "^2.4.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -616,7 +628,7 @@
 				"url": "https://opencollective.com/typescript-eslint"
 			},
 			"peerDependencies": {
-				"@typescript-eslint/parser": "^8.46.0",
+				"@typescript-eslint/parser": "^8.54.0",
 				"eslint": "^8.57.0 || ^9.0.0",
 				"typescript": ">=4.8.4 <6.0.0"
 			}
@@ -631,16 +643,16 @@
 			}
 		},
 		"node_modules/@typescript-eslint/parser": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
-			"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
+			"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/scope-manager": "8.46.0",
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/typescript-estree": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0",
-				"debug": "^4.3.4"
+				"@typescript-eslint/scope-manager": "8.54.0",
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/typescript-estree": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0",
+				"debug": "^4.4.3"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -655,14 +667,14 @@
 			}
 		},
 		"node_modules/@typescript-eslint/project-service": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
-			"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
+			"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/tsconfig-utils": "^8.46.0",
-				"@typescript-eslint/types": "^8.46.0",
-				"debug": "^4.3.4"
+				"@typescript-eslint/tsconfig-utils": "^8.54.0",
+				"@typescript-eslint/types": "^8.54.0",
+				"debug": "^4.4.3"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -676,13 +688,13 @@
 			}
 		},
 		"node_modules/@typescript-eslint/scope-manager": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
-			"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
+			"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0"
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -693,9 +705,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/tsconfig-utils": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
-			"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
+			"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
 			"dev": true,
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -709,16 +721,16 @@
 			}
 		},
 		"node_modules/@typescript-eslint/type-utils": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
-			"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
+			"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/typescript-estree": "8.46.0",
-				"@typescript-eslint/utils": "8.46.0",
-				"debug": "^4.3.4",
-				"ts-api-utils": "^2.1.0"
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/typescript-estree": "8.54.0",
+				"@typescript-eslint/utils": "8.54.0",
+				"debug": "^4.4.3",
+				"ts-api-utils": "^2.4.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -733,9 +745,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/types": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
-			"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
+			"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
 			"dev": true,
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -746,21 +758,20 @@
 			}
 		},
 		"node_modules/@typescript-eslint/typescript-estree": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
-			"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
+			"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/project-service": "8.46.0",
-				"@typescript-eslint/tsconfig-utils": "8.46.0",
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0",
-				"debug": "^4.3.4",
-				"fast-glob": "^3.3.2",
-				"is-glob": "^4.0.3",
-				"minimatch": "^9.0.4",
-				"semver": "^7.6.0",
-				"ts-api-utils": "^2.1.0"
+				"@typescript-eslint/project-service": "8.54.0",
+				"@typescript-eslint/tsconfig-utils": "8.54.0",
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0",
+				"debug": "^4.4.3",
+				"minimatch": "^9.0.5",
+				"semver": "^7.7.3",
+				"tinyglobby": "^0.2.15",
+				"ts-api-utils": "^2.4.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -774,9 +785,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
-			"version": "2.0.3",
-			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
-			"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+			"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
 			"dev": true,
 			"dependencies": {
 				"balanced-match": "^1.0.0"
@@ -798,15 +809,15 @@
 			}
 		},
 		"node_modules/@typescript-eslint/utils": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
-			"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
+			"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
 			"dev": true,
 			"dependencies": {
-				"@eslint-community/eslint-utils": "^4.7.0",
-				"@typescript-eslint/scope-manager": "8.46.0",
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/typescript-estree": "8.46.0"
+				"@eslint-community/eslint-utils": "^4.9.1",
+				"@typescript-eslint/scope-manager": "8.54.0",
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/typescript-estree": "8.54.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -821,12 +832,12 @@
 			}
 		},
 		"node_modules/@typescript-eslint/visitor-keys": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
-			"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
+			"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.46.0",
+				"@typescript-eslint/types": "8.54.0",
 				"eslint-visitor-keys": "^4.2.1"
 			},
 			"engines": {
@@ -872,9 +883,9 @@
 			"dev": true
 		},
 		"node_modules/acorn": {
-			"version": "8.15.0",
-			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
-			"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+			"version": "8.16.0",
+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+			"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
 			"dev": true,
 			"bin": {
 				"acorn": "bin/acorn"
@@ -1004,6 +1015,12 @@
 				"@mdn/browser-compat-data": "^5.6.19"
 			}
 		},
+		"node_modules/ast-metadata-inferer/node_modules/@mdn/browser-compat-data": {
+			"version": "5.7.6",
+			"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz",
+			"integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==",
+			"dev": true
+		},
 		"node_modules/astral-regex": {
 			"version": "2.0.0",
 			"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -1263,9 +1280,9 @@
 			}
 		},
 		"node_modules/comment-parser": {
-			"version": "1.4.1",
-			"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
-			"integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==",
+			"version": "1.4.6",
+			"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz",
+			"integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==",
 			"dev": true,
 			"engines": {
 				"node": ">= 12.0.0"
@@ -1612,13 +1629,13 @@
 			}
 		},
 		"node_modules/enhanced-resolve": {
-			"version": "5.18.3",
-			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
-			"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+			"version": "5.21.0",
+			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
+			"integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
 			"dev": true,
 			"dependencies": {
 				"graceful-fs": "^4.2.4",
-				"tapable": "^2.2.0"
+				"tapable": "^2.3.3"
 			},
 			"engines": {
 				"node": ">=10.13.0"
@@ -1746,46 +1763,47 @@
 			}
 		},
 		"node_modules/eslint-config-wikimedia": {
-			"version": "0.32.3",
-			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.32.3.tgz",
-			"integrity": "sha512-Ekz2/ozpCCjQl3VbC6dW7ChqoW7FRilLDxmJ+FJOZhIxxzZSZR5QqQOAGWSZAlG1ONkZbYV/TPwGLWZcrNxyaA==",
+			"version": "0.32.4",
+			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.32.4.tgz",
+			"integrity": "sha512-zcHJYss2vo8HK5PzkFuaV9mzaSGRuhA+jFGoQ4rNIwWz0usZsuQ2LYpkKxrbCVX1CbV0PzG+jJ6p0cLI+G37JQ==",
 			"dev": true,
 			"dependencies": {
 				"@stylistic/eslint-plugin": "^3.1.0",
-				"@typescript-eslint/eslint-plugin": "8.46.0",
-				"@typescript-eslint/parser": "8.46.0",
+				"@typescript-eslint/eslint-plugin": "8.54.0",
+				"@typescript-eslint/parser": "8.54.0",
 				"browserslist-config-wikimedia": "^0.7.0",
-				"eslint": "^8.57.0",
-				"eslint-plugin-compat": "^6.0.2",
+				"eslint-plugin-compat": "^6.1.0",
 				"eslint-plugin-es-x": "^8.7.0",
-				"eslint-plugin-jest": "^29.0.1",
-				"eslint-plugin-jsdoc": "61.3.0",
+				"eslint-plugin-jest": "^29.12.2",
+				"eslint-plugin-jsdoc": "^62.9.0",
 				"eslint-plugin-json-es": "^1.6.0",
-				"eslint-plugin-mediawiki": "^0.8.2",
+				"eslint-plugin-mediawiki": "^0.8.3",
 				"eslint-plugin-mocha": "^10.5.0",
-				"eslint-plugin-n": "^17.23.1",
-				"eslint-plugin-no-jquery": "^3.1.1",
-				"eslint-plugin-qunit": "^8.2.5",
-				"eslint-plugin-security": "^3.0.1",
+				"eslint-plugin-n": "^17.24.0",
+				"eslint-plugin-no-jquery": "^4.0.0",
+				"eslint-plugin-qunit": "^8.2.6",
+				"eslint-plugin-security": "^4.0.0",
 				"eslint-plugin-unicorn": "^56.0.1",
 				"eslint-plugin-vue": "^9.33.0",
-				"eslint-plugin-wdio": "^9.16.2",
+				"eslint-plugin-wdio": "9.23.0",
 				"eslint-plugin-yml": "^1.19.0"
 			},
 			"engines": {
 				"node": ">=20 <25"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0"
 			}
 		},
 		"node_modules/eslint-plugin-compat": {
-			"version": "6.0.2",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.0.2.tgz",
-			"integrity": "sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==",
+			"version": "6.2.1",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.2.1.tgz",
+			"integrity": "sha512-gLKqUH+lQcCL+HzsROUjBDvakc5Zaga51Y4ZAkPCXc41pzKBfyluqTr2j8zOx8QQQb7zyglu1LVoL5aSNWf2SQ==",
 			"dev": true,
 			"dependencies": {
-				"@mdn/browser-compat-data": "^5.5.35",
+				"@mdn/browser-compat-data": "^6.1.1",
 				"ast-metadata-inferer": "^0.8.1",
-				"browserslist": "^4.24.2",
-				"caniuse-lite": "^1.0.30001687",
+				"browserslist": "^4.25.2",
 				"find-up": "^5.0.0",
 				"globals": "^15.7.0",
 				"lodash.memoize": "^4.1.2",
@@ -1795,7 +1813,7 @@
 				"node": ">=18.x"
 			},
 			"peerDependencies": {
-				"eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+				"eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
 			}
 		},
 		"node_modules/eslint-plugin-compat/node_modules/globals": {
@@ -1861,57 +1879,57 @@
 			}
 		},
 		"node_modules/eslint-plugin-jsdoc": {
-			"version": "61.3.0",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.3.0.tgz",
-			"integrity": "sha512-E4m/5J5lrasd63Z74q4CCZ4PFnywnnrcvA7zZ98802NPhrZKKTp5NH+XAT+afcjXp2ps2/OQF5gPSWCT2XFCJg==",
+			"version": "62.9.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz",
+			"integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==",
 			"dev": true,
 			"dependencies": {
-				"@es-joy/jsdoccomment": "~0.76.0",
+				"@es-joy/jsdoccomment": "~0.86.0",
 				"@es-joy/resolve.exports": "1.2.0",
 				"are-docs-informative": "^0.0.2",
-				"comment-parser": "1.4.1",
+				"comment-parser": "1.4.6",
 				"debug": "^4.4.3",
 				"escape-string-regexp": "^4.0.0",
-				"espree": "^10.4.0",
-				"esquery": "^1.6.0",
+				"espree": "^11.2.0",
+				"esquery": "^1.7.0",
 				"html-entities": "^2.6.0",
 				"object-deep-merge": "^2.0.0",
 				"parse-imports-exports": "^0.2.4",
-				"semver": "^7.7.3",
+				"semver": "^7.7.4",
 				"spdx-expression-parse": "^4.0.0",
 				"to-valid-identifier": "^1.0.0"
 			},
 			"engines": {
-				"node": ">=20.11.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
 			},
 			"peerDependencies": {
-				"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+				"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
 			}
 		},
 		"node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": {
-			"version": "4.2.1",
-			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
-			"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+			"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
 			"dev": true,
 			"engines": {
-				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
 			},
 			"funding": {
 				"url": "https://opencollective.com/eslint"
 			}
 		},
 		"node_modules/eslint-plugin-jsdoc/node_modules/espree": {
-			"version": "10.4.0",
-			"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
-			"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+			"version": "11.2.0",
+			"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+			"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
 			"dev": true,
 			"dependencies": {
-				"acorn": "^8.15.0",
+				"acorn": "^8.16.0",
 				"acorn-jsx": "^5.3.2",
-				"eslint-visitor-keys": "^4.2.1"
+				"eslint-visitor-keys": "^5.0.1"
 			},
 			"engines": {
-				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
 			},
 			"funding": {
 				"url": "https://opencollective.com/eslint"
@@ -1931,9 +1949,9 @@
 			}
 		},
 		"node_modules/eslint-plugin-mediawiki": {
-			"version": "0.8.2",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.8.2.tgz",
-			"integrity": "sha512-ydYrpkzm8IVVDQA96QPF3HnFd2xjkIEh7gixD2gvOqUbUZF0p36LtpWXOFAlPWAvHLePWbNNTD5ovd3d4hEtog==",
+			"version": "0.8.3",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.8.3.tgz",
+			"integrity": "sha512-RQKZd40C1taMDk5N9+aFLEBGBB95RNG7Gc54EsJ8pHsJu8//nIdpxNFWPtQz6RNxz6pZUXBnMCxzkMOLM3Mm1w==",
 			"dev": true,
 			"dependencies": {
 				"upath": "^2.0.1"
@@ -1960,9 +1978,9 @@
 			}
 		},
 		"node_modules/eslint-plugin-n": {
-			"version": "17.23.1",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.23.1.tgz",
-			"integrity": "sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==",
+			"version": "17.24.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz",
+			"integrity": "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==",
 			"dev": true,
 			"dependencies": {
 				"@eslint-community/eslint-utils": "^4.5.0",
@@ -2034,31 +2052,34 @@
 			}
 		},
 		"node_modules/eslint-plugin-no-jquery": {
-			"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==",
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-4.0.0.tgz",
+			"integrity": "sha512-ZR631D3qIQfgjKOAcgvYa5cB8xdTvFXAD5MbK5x5WltLSwFxmGnoaTXNtnptFU7py07ALrIe5dZRYncu4RD/Ug==",
 			"dev": true,
 			"peerDependencies": {
-				"eslint": ">=8.0.0"
+				"eslint": ">=8.0.0 <9.0.0"
 			}
 		},
 		"node_modules/eslint-plugin-qunit": {
-			"version": "8.2.5",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-qunit/-/eslint-plugin-qunit-8.2.5.tgz",
-			"integrity": "sha512-qr7RJCYImKQjB+39q4q46i1l7p1V3joHzBE5CAYfxn5tfVFjrnjn/tw7q/kDyweU9kAIcLul0Dx/KWVUCb3BgA==",
+			"version": "8.2.6",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-qunit/-/eslint-plugin-qunit-8.2.6.tgz",
+			"integrity": "sha512-S1jC/DIW9J8VtNX4uG1vlf5FZVrfQFlcuiYmvTHR2IICUhubHqpWA5o+qS1tujh+81Gs39omKV2D4OXfbSJE5g==",
 			"dev": true,
 			"dependencies": {
-				"eslint-utils": "^3.0.0",
+				"@eslint-community/eslint-utils": "^4.4.0",
 				"requireindex": "^1.2.0"
 			},
 			"engines": {
 				"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
+			},
+			"peerDependencies": {
+				"eslint": ">=8.38.0"
 			}
 		},
 		"node_modules/eslint-plugin-security": {
-			"version": "3.0.1",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz",
-			"integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==",
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.0.tgz",
+			"integrity": "sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==",
 			"dev": true,
 			"dependencies": {
 				"safe-regex": "^2.1.1"
@@ -2138,9 +2159,9 @@
 			}
 		},
 		"node_modules/eslint-plugin-wdio": {
-			"version": "9.16.2",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-wdio/-/eslint-plugin-wdio-9.16.2.tgz",
-			"integrity": "sha512-qkqsPgxN70OnUPWMjmzJbSbvm2+Q087JIGss53/OFI4Y46xKlV5VLhLiYealaAibAiXmnfWKd0tERjZAzVL87A==",
+			"version": "9.23.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-wdio/-/eslint-plugin-wdio-9.23.0.tgz",
+			"integrity": "sha512-8tcpupzp2Qmv+uSfhzeHi42LVA9PyjkpMBPclSIkPxBfXpj4fMrejwAHu1PROh1OmJN1VQcGQUTWvSzyRcV2vA==",
 			"dev": true,
 			"engines": {
 				"node": ">=18.20.0"
@@ -2285,9 +2306,9 @@
 			}
 		},
 		"node_modules/esquery": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
-			"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+			"version": "1.7.0",
+			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+			"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
 			"dev": true,
 			"dependencies": {
 				"estraverse": "^5.1.0"
@@ -2582,9 +2603,9 @@
 			}
 		},
 		"node_modules/get-tsconfig": {
-			"version": "4.13.0",
-			"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
-			"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+			"version": "4.14.0",
+			"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+			"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
 			"dev": true,
 			"dependencies": {
 				"resolve-pkg-maps": "^1.0.0"
@@ -3295,9 +3316,9 @@
 			"dev": true
 		},
 		"node_modules/jsdoc-type-pratt-parser": {
-			"version": "6.10.0",
-			"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-6.10.0.tgz",
-			"integrity": "sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==",
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz",
+			"integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==",
 			"dev": true,
 			"engines": {
 				"node": ">=20.0.0"
@@ -4087,9 +4108,9 @@
 			}
 		},
 		"node_modules/postcss": {
-			"version": "8.5.6",
-			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-			"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+			"version": "8.5.14",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+			"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
 			"dev": true,
 			"funding": [
 				{
@@ -4607,9 +4628,9 @@
 			"dev": true
 		},
 		"node_modules/semver": {
-			"version": "7.7.3",
-			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
-			"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+			"version": "7.7.4",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+			"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
 			"dev": true,
 			"bin": {
 				"semver": "bin/semver.js"
@@ -5169,9 +5190,9 @@
 			"dev": true
 		},
 		"node_modules/tapable": {
-			"version": "2.3.0",
-			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
-			"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+			"version": "2.3.3",
+			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+			"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
 			"dev": true,
 			"engines": {
 				"node": ">=6"
@@ -5187,6 +5208,51 @@
 			"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
 			"dev": true
 		},
+		"node_modules/tinyglobby": {
+			"version": "0.2.16",
+			"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+			"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+			"dev": true,
+			"dependencies": {
+				"fdir": "^6.5.0",
+				"picomatch": "^4.0.4"
+			},
+			"engines": {
+				"node": ">=12.0.0"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/SuperchupuDev"
+			}
+		},
+		"node_modules/tinyglobby/node_modules/fdir": {
+			"version": "6.5.0",
+			"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+			"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+			"dev": true,
+			"engines": {
+				"node": ">=12.0.0"
+			},
+			"peerDependencies": {
+				"picomatch": "^3 || ^4"
+			},
+			"peerDependenciesMeta": {
+				"picomatch": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/tinyglobby/node_modules/picomatch": {
+			"version": "4.0.4",
+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+			"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+			"dev": true,
+			"engines": {
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/jonschlinkert"
+			}
+		},
 		"node_modules/to-regex-range": {
 			"version": "5.0.1",
 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -5216,9 +5282,9 @@
 			}
 		},
 		"node_modules/ts-api-utils": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
-			"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+			"version": "2.5.0",
+			"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+			"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
 			"dev": true,
 			"engines": {
 				"node": ">=18.12"
diff --git a/package.json b/package.json
index ae51d19..8cf0146 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
 		"doc": "jsdoc -c jsdoc.json"
 	},
 	"devDependencies": {
-		"eslint-config-wikimedia": "0.32.3",
+		"eslint-config-wikimedia": "0.32.4",
 		"grunt": "1.6.2",
 		"grunt-banana-checker": "0.13.0",
 		"grunt-eslint": "24.3.0",
-- 
2.47.3

$ date
--- stdout ---
Tue May  5 05:21:35 UTC 2026

--- end ---
$ git clone file:///srv/git/mediawiki-extensions-Wikispeech.git /src/repo --depth=1 -b master
--- stderr ---
Cloning into '/src/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 ---
8fd73e699f99571dca1e64f7e4fe4d23befa582b refs/heads/master

--- end ---
$ /usr/bin/npm audit --json
--- stdout ---
{
  "auditReportVersion": 2,
  "vulnerabilities": {
    "postcss": {
      "name": "postcss",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        {
          "source": 1117015,
          "name": "postcss",
          "dependency": "postcss",
          "title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output",
          "url": "https://github.com/advisories/GHSA-qx2v-qp2m-jg93",
          "severity": "moderate",
          "cwe": [
            "CWE-79"
          ],
          "cvss": {
            "score": 6.1,
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
          },
          "range": "<8.5.10"
        }
      ],
      "effects": [],
      "range": "<8.5.10",
      "nodes": [
        "node_modules/postcss"
      ],
      "fixAvailable": true
    }
  },
  "metadata": {
    "vulnerabilities": {
      "info": 0,
      "low": 0,
      "moderate": 1,
      "high": 0,
      "critical": 0,
      "total": 1
    },
    "dependencies": {
      "prod": 1,
      "dev": 461,
      "optional": 0,
      "peer": 1,
      "peerOptional": 0,
      "total": 461
    }
  }
}

--- 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: 37 installs, 0 updates, 0 removals
  - Locking composer/pcre (3.3.2)
  - Locking composer/semver (3.4.4)
  - Locking composer/spdx-licenses (1.5.10)
  - Locking composer/xdebug-handler (3.0.5)
  - Locking danog/advanced-json-rpc (v3.2.3)
  - Locking dealerdirect/phpcodesniffer-composer-installer (v1.2.0)
  - Locking doctrine/deprecations (1.1.6)
  - Locking mediawiki/mediawiki-codesniffer (v50.0.0)
  - Locking mediawiki/mediawiki-phan-config (0.20.0)
  - Locking mediawiki/minus-x (2.0.1)
  - Locking mediawiki/phan-taint-check-plugin (9.1.0)
  - Locking netresearch/jsonmapper (v5.0.1)
  - Locking phan/phan (6.0.2)
  - Locking phan/tolerant-php-parser (v0.2.0)
  - Locking phan/var_representation_polyfill (0.1.4)
  - 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.4.0)
  - Locking phpcsstandards/phpcsutils (1.2.2)
  - Locking phpdocumentor/reflection-common (2.2.0)
  - Locking phpdocumentor/reflection-docblock (6.0.3)
  - Locking phpdocumentor/type-resolver (2.0.0)
  - Locking phpstan/phpdoc-parser (2.3.2)
  - Locking psr/container (2.0.2)
  - Locking psr/log (3.0.2)
  - Locking sabre/event (6.1.0)
  - Locking squizlabs/php_codesniffer (3.13.5)
  - Locking symfony/console (v8.0.9)
  - Locking symfony/deprecation-contracts (v3.6.0)
  - Locking symfony/polyfill-ctype (v1.37.0)
  - Locking symfony/polyfill-intl-grapheme (v1.37.0)
  - Locking symfony/polyfill-intl-normalizer (v1.37.0)
  - Locking symfony/polyfill-mbstring (v1.37.0)
  - Locking symfony/service-contracts (v3.6.1)
  - Locking symfony/string (v8.0.8)
  - Locking webmozart/assert (2.3.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 37 installs, 0 updates, 0 removals
    0 [>---------------------------]    0 [->--------------------------]
  - Installing squizlabs/php_codesniffer (3.13.5): Extracting archive
  - Installing dealerdirect/phpcodesniffer-composer-installer (v1.2.0): Extracting archive
  - Installing composer/pcre (3.3.2): Extracting archive
  - Installing phpcsstandards/phpcsutils (1.2.2): Extracting archive
  - Installing phpcsstandards/phpcsextra (1.4.0): Extracting archive
  - Installing symfony/polyfill-mbstring (v1.37.0): Extracting archive
  - Installing composer/spdx-licenses (1.5.10): Extracting archive
  - Installing composer/semver (3.4.4): Extracting archive
  - Installing mediawiki/mediawiki-codesniffer (v50.0.0): Extracting archive
  - Installing symfony/polyfill-intl-normalizer (v1.37.0): Extracting archive
  - Installing symfony/polyfill-intl-grapheme (v1.37.0): Extracting archive
  - Installing symfony/polyfill-ctype (v1.37.0): Extracting archive
  - Installing symfony/string (v8.0.8): Extracting archive
  - Installing symfony/deprecation-contracts (v3.6.0): Extracting archive
  - Installing psr/container (2.0.2): Extracting archive
  - Installing symfony/service-contracts (v3.6.1): Extracting archive
  - Installing symfony/console (v8.0.9): Extracting archive
  - Installing sabre/event (6.1.0): Extracting archive
  - Installing phan/var_representation_polyfill (0.1.4): Extracting archive
  - Installing phan/tolerant-php-parser (v0.2.0): Extracting archive
  - Installing netresearch/jsonmapper (v5.0.1): Extracting archive
  - Installing webmozart/assert (2.3.0): Extracting archive
  - Installing phpstan/phpdoc-parser (2.3.2): Extracting archive
  - Installing phpdocumentor/reflection-common (2.2.0): Extracting archive
  - Installing doctrine/deprecations (1.1.6): Extracting archive
  - Installing phpdocumentor/type-resolver (2.0.0): Extracting archive
  - Installing phpdocumentor/reflection-docblock (6.0.3): Extracting archive
  - Installing danog/advanced-json-rpc (v3.2.3): Extracting archive
  - Installing psr/log (3.0.2): Extracting archive
  - Installing composer/xdebug-handler (3.0.5): Extracting archive
  - Installing phan/phan (6.0.2): Extracting archive
  - Installing mediawiki/phan-taint-check-plugin (9.1.0): Extracting archive
  - Installing mediawiki/mediawiki-phan-config (0.20.0): Extracting archive
  - Installing mediawiki/minus-x (2.0.1): 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/35 [>---------------------------]   0%
 28/35 [======================>-----]  80%
 35/35 [============================] 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.32.3 -> 0.32.4
$ /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 466 packages, and audited 467 packages in 6s

109 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

--- end ---
$ package-lock-lint /src/repo/package-lock.json
--- stdout ---
Checking /src/repo/package-lock.json

--- end ---
$ /usr/bin/npm install grunt-eslint@24.3.0 --save-exact
--- stdout ---

up to date, audited 467 packages in 1s

109 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

--- end ---
$ package-lock-lint /src/repo/package-lock.json
--- stdout ---
Checking /src/repo/package-lock.json

--- end ---
$ ./node_modules/.bin/eslint . --fix
--- stdout ---

/src/repo/dev/speechoid-docker-compose/docker-compose.yml
  19:1  warning  This line has a length of 118. Maximum allowed is 100  max-len
  24:1  warning  This line has a length of 117. Maximum allowed is 100  max-len
  30:1  warning  This line has a length of 118. Maximum allowed is 100  max-len
  49:1  warning  This line has a length of 119. Maximum allowed is 100  max-len
  56:1  warning  This line has a length of 117. Maximum allowed is 100  max-len
  70:1  warning  This line has a length of 107. Maximum allowed is 100  max-len
  79:1  warning  This line has a length of 127. Maximum allowed is 100  max-len

/src/repo/modules/ext.wikispeech.feedback.js
  145:1  warning  Missing JSDoc @param "params.storage" type             jsdoc/require-param-type
  146:1  warning  Missing JSDoc @param "params.selectionPlayer" type     jsdoc/require-param-type
  220:1  warning  This line has a length of 102. Maximum allowed is 100  max-len
  228:3  warning  Prefer .then to .done                                  no-jquery/no-done-fail
  229:4  warning  Prefer .then to .done                                  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.gadget.js
  33:1  warning  Missing JSDoc @param "mainInstance" type  jsdoc/require-param-type
  39:3  warning  Prefer .then to .done                     no-jquery/no-done-fail

/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
  129:1  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.player.js
  246:3  warning  Prefer .then to .done  no-jquery/no-done-fail
  246:3  warning  Prefer .then to .fail  no-jquery/no-done-fail
  259:5  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.selectionPlayer.js
  124:3  warning  Prefer .then to .done  no-jquery/no-done-fail
  158:3  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.sharedUserOptionSettings.js
   19:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
   31:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
   31:3  warning  Prefer .then to .fail                                     no-jquery/no-done-fail
   69:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
   70:1  warning  Missing JSDoc @param "isProducer" type                    jsdoc/require-param-type
   89:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
  115:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
  116:1  warning  The type 'ext.wikispeech.UserOptionsDialog' is undefined  jsdoc/no-undefined-types
  117:1  warning  Missing JSDoc @param "isProducer" type                    jsdoc/require-param-type
  133:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
  133:3  warning  Prefer .then to .fail                                     no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.storage.js
  259:19  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.transcriptionPreviewer.js
  37:1   warning  This line has a length of 103. Maximum allowed is 100  max-len
  66:19  warning  Prefer .then to .done                                  no-jquery/no-done-fail
  66:19  warning  Prefer .then to .fail                                  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.ui.js
   68:18  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  199:3   warning  Prefer .then to .done                                                                                no-jquery/no-done-fail
  249:1   warning  This line has a length of 107. Maximum allowed is 100                                                max-len
  331:1   warning  This line has a length of 116. Maximum allowed is 100                                                max-len
  428:30  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  436:23  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  742:21  warning  Found non-literal argument in require                                                                security/detect-non-literal-require

/src/repo/modules/ext.wikispeech.userOptionsDialog.js
    7:1  warning  This line has a length of 102. Maximum allowed is 100  max-len
  132:3  warning  Prefer .then to .done                                  no-jquery/no-done-fail

/src/repo/tests/qunit/ext.wikispeech.ui.test.js
  67:2  warning  Prefer .then to .done  no-jquery/no-done-fail

✖ 46 problems (0 errors, 46 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 118. Maximum allowed is 100.","line":19,"column":1,"nodeType":"Program","messageId":"max","endLine":19,"endColumn":119},{"ruleId":"max-len","severity":1,"message":"This line has a length of 117. Maximum allowed is 100.","line":24,"column":1,"nodeType":"Program","messageId":"max","endLine":24,"endColumn":118},{"ruleId":"max-len","severity":1,"message":"This line has a length of 118. Maximum allowed is 100.","line":30,"column":1,"nodeType":"Program","messageId":"max","endLine":30,"endColumn":119},{"ruleId":"max-len","severity":1,"message":"This line has a length of 119. Maximum allowed is 100.","line":49,"column":1,"nodeType":"Program","messageId":"max","endLine":49,"endColumn":120},{"ruleId":"max-len","severity":1,"message":"This line has a length of 117. Maximum allowed is 100.","line":56,"column":1,"nodeType":"Program","messageId":"max","endLine":56,"endColumn":118},{"ruleId":"max-len","severity":1,"message":"This line has a length of 107. Maximum allowed is 100.","line":70,"column":1,"nodeType":"Program","messageId":"max","endLine":70,"endColumn":108},{"ruleId":"max-len","severity":1,"message":"This line has a length of 127. Maximum allowed is 100.","line":79,"column":1,"nodeType":"Program","messageId":"max","endLine":79,"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  # Disabled for now, see https://phabricator.wikimedia.org/T384313.\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      - ./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/audio/error.en.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/audio/error.sv.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.feedback.js","messages":[{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"params.storage\" type.","line":145,"column":1,"nodeType":"Block","endLine":145,"endColumn":1},{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"params.selectionPlayer\" type.","line":146,"column":1,"nodeType":"Block","endLine":146,"endColumn":1},{"ruleId":"max-len","severity":1,"message":"This line has a length of 102. Maximum allowed is 100.","line":220,"column":1,"nodeType":"Program","messageId":"max","endLine":220,"endColumn":97},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":228,"column":3,"nodeType":"CallExpression","endLine":248,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":229,"column":4,"nodeType":"CallExpression","endLine":247,"endColumn":7}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":5,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * @module ext.wikispeech.feedback\n */\n\n/**\n * Dialog for reporting pronunciation errors.\n *\n * @extends OO.ui.ProcessDialog\n *\n * @param {Object} config\n */\nfunction PronunciationErrorDialog( config ) {\n\tconfig = config || {};\n\tOO.ui.ProcessDialog.call( this, config );\n\tthis.selectedWord = config.selectedWord;\n\tthis.context = config.context;\n}\n\nOO.inheritClass( PronunciationErrorDialog, OO.ui.ProcessDialog );\n\nPronunciationErrorDialog.static.name = 'pronunciationErrorDialog';\nPronunciationErrorDialog.static.title = mw.msg( 'wikispeech-report-pronunciation-error' );\nPronunciationErrorDialog.static.actions = [\n\t{ action: 'submit', label: mw.msg( 'wikispeech-report-pronunciation-error-button' ), flags: 'primary' },\n\t{ action: 'cancel', label: mw.msg( 'ooui-dialog-message-reject' ), flags: 'safe' }\n];\n\nPronunciationErrorDialog.prototype.initialize = function () {\n\tPronunciationErrorDialog.super.prototype.initialize.apply( this, arguments );\n\n\tthis.wordInput = new OO.ui.TextInputWidget( {\n\t\tvalue: this.selectedWord\n\t} );\n\n\tthis.contextInput = new OO.ui.TextInputWidget( {\n\t\tvalue: this.context\n\t} );\n\n\tthis.extraInput = new OO.ui.TextInputWidget();\n\n\tthis.content = new OO.ui.PanelLayout( {\n\t\tpadded: true,\n\t\texpanded: false\n\t} );\n\n\tthis.content.$element.append(\n\t\t$( '<p>' ).text( mw.msg( 'wikispeech-word' ) ),\n\t\tthis.wordInput.$element,\n\t\t$( '<p>' ).text( mw.msg( 'wikispeech-report-pronunciation-error-context' ) ),\n\t\tthis.contextInput.$element,\n\t\t$( '<p>' ).text( mw.msg( 'wikispeech-report-pronunciation-error-other' ) ),\n\t\tthis.extraInput.$element\n\t);\n\n\tthis.$body.append( this.content.$element );\n};\n\n/**\n * Handle dialog actions.\n *\n * @param {string} action\n * @return {OO.ui.Process}\n */\nPronunciationErrorDialog.prototype.getActionProcess = function ( action ) {\n\tif ( action === 'submit' ) {\n\t\treturn new OO.ui.Process( () => {\n\t\t\tthis.close( {\n\t\t\t\taction: 'submit',\n\t\t\t\tselectedText: this.wordInput.getValue(),\n\t\t\t\tcontext: this.contextInput.getValue(),\n\t\t\t\textra: this.extraInput.getValue()\n\t\t\t} );\n\t\t} );\n\t} else if ( action === 'cancel' ) {\n\t\treturn new OO.ui.Process( () => {\n\t\t\tthis.close( {\n\t\t\t\taction: 'cancel'\n\t\t\t} );\n\t\t} );\n\t}\n\treturn PronunciationErrorDialog.super.prototype.getActionProcess.call( this, action );\n};\n\n/**\n * Report a pronunciation error by adding a row to a wiki page.\n *\n * @param {Object} options\n * @return {Promise}\n */\nfunction reportPronunciationError( options ) {\n\tconst producerUrl = mw.config.get( 'wgWikispeechProducerUrl' );\n\tlet api;\n\n\tif ( producerUrl ) {\n\t\tconst reportPronunciationUrl = mw.config.get( 'wgWikispeechReportPronunciationUrl' );\n\t\tapi = new mw.ForeignApi( reportPronunciationUrl );\n\t} else {\n\t\tapi = new mw.Api();\n\t}\n\n\tconst pageTitle = options.pageTitle;\n\n\treturn api.get( {\n\t\taction: 'query',\n\t\tprop: 'revisions',\n\t\trvprop: 'content',\n\t\ttitles: pageTitle,\n\t\tformatversion: 2,\n\t\tformat: 'json'\n\t} ).then( ( data ) => {\n\t\tconst page = data.query.pages[ 0 ];\n\t\tif ( page.missing ) {\n\t\t\tthrow new Error( mw.msg( 'wikispeech-non-existing-page' ) );\n\t\t}\n\t\tconst content = page.revisions[ 0 ].content;\n\n\t\tconst tableEndIndex = content.lastIndexOf( '|}' );\n\t\tif ( tableEndIndex === -1 ) {\n\t\t\tthrow new Error( mw.msg( 'wikispeech-report-table-missing' ) );\n\t\t}\n\n\t\tconst row = `|-\\n| ${ options.date } || ${ options.word } || [${ options.fullUrl } ${ options.pageName.replace( /_/g, ' ' ) } ] || ${ options.context } || ${ options.extra } ||\\n`;\n\n\t\tconst newContent =\n\t\t\tcontent.slice( 0, tableEndIndex ) +\n\t\t\trow +\n\t\t\tcontent.slice( tableEndIndex );\n\n\t\treturn api.postWithToken( 'csrf', {\n\t\t\taction: 'edit',\n\t\t\ttitle: pageTitle,\n\t\t\ttext: newContent,\n\t\t\tformat: 'json'\n\t\t} );\n\t} );\n}\n\nconst feedback = {};\n\n/**\n * Open the pronunciation error dialog and handle submission.\n * if a valid word is selected, it is pre filled.\n *\n * @param {Object} params\n * @param params.storage\n * @param params.selectionPlayer\n */\nfeedback.openPronunciationErrorDialog = function ( { storage, selectionPlayer } ) {\n\tconst selection = window.getSelection();\n\n\tconst server = mw.config.get( 'wgServer' );\n\tconst articlePath = mw.config.get( 'wgArticlePath' );\n\tconst pageName = mw.config.get( 'wgPageName' );\n\tconst fullUrl = server + articlePath.replace( '$1', pageName );\n\n\tconst lang = mw.config.get( 'wgContentLanguage' );\n\tconst feedbackPage = mw.config.get( 'wgWikispeechFeedbackPage' ).replace( '$lang', lang );\n\n\tlet selectedWord = '';\n\tlet context = '';\n\n\tconst openDialog = () => {\n\t\tconst dialog = new PronunciationErrorDialog( { selectedWord, context } );\n\t\tconst windowManager = new OO.ui.WindowManager();\n\t\t$( document.body ).append( windowManager.$element );\n\t\twindowManager.addWindows( [ dialog ] );\n\t\twindowManager.openWindow( dialog ).closed.then( ( data ) => {\n\t\t\tif ( data && data.action === 'submit' ) {\n\t\t\t\tconst word = data.selectedText || selectedWord;\n\t\t\t\tconst contextValue = data.context || context;\n\t\t\t\tconst extra = data.extra;\n\t\t\t\tconst date = new Date().toISOString().split( 'T' )[ 0 ];\n\n\t\t\t\treportPronunciationError( {\n\t\t\t\t\tpageTitle: feedbackPage,\n\t\t\t\t\tword,\n\t\t\t\t\tcontext: contextValue,\n\t\t\t\t\textra,\n\t\t\t\t\tdate,\n\t\t\t\t\tfullUrl,\n\t\t\t\t\tpageName\n\t\t\t\t} ).then( () => {\n\t\t\t\t\tconst pageTitle = feedbackPage.replace( /_/g, ' ' );\n\t\t\t\t\tlet pageUrl;\n\t\t\t\t\tconst producerUrl = mw.config.get( 'wgWikispeechProducerUrl' );\n\n\t\t\t\t\tif ( producerUrl ) {\n\t\t\t\t\t\tconst apiUrl = mw.config.get( 'wgWikispeechReportPronunciationUrl' );\n\t\t\t\t\t\tconst producerBase = apiUrl.replace( /\\/api\\.php$/, '' );\n\n\t\t\t\t\t\tpageUrl = producerBase + '/index.php?' +\n\t\t\t\t\t\tnew URLSearchParams( { title: feedbackPage } );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpageUrl = mw.util.getUrl( feedbackPage );\n\t\t\t\t\t}\n\n\t\t\t\t\tconst $link = $( '<a>' )\n\t\t\t\t\t\t.attr( {\n\t\t\t\t\t\t\thref: pageUrl,\n\t\t\t\t\t\t\ttarget: '_blank'\n\t\t\t\t\t\t} )\n\t\t\t\t\t\t.text( pageTitle );\n\n\t\t\t\t\tconst message = mw.msg( 'wikispeech-pronunciation-saved' );\n\t\t\t\t\tmw.notify(\n\t\t\t\t\t\t$( '<div>' ).text( message + ' ' ).append( $link ),\n\t\t\t\t\t\t{ type: 'success' }\n\t\t\t\t\t);\n\t\t\t\t} ).catch( ( err ) => {\n\t\t\t\t\tmw.log.warn( err );\n\t\t\t\t\tmw.notify( err && err.message ? err.message : mw.msg( 'wikispeech-report-pronunciation-post-error' ), { type: 'error' } );\n\t\t\t\t} );\n\n\t\t\t}\n\t\t} );\n\t};\n\n\tif ( selectionPlayer.isSelectionValid() ) {\n\t\tconst range = selection.getRangeAt( 0 );\n\t\tconst startNode = storage.getFirstTextNode( selectionPlayer.getFirstNodeInSelection(), true );\n\t\tconst endNode = storage.getLastTextNode( selectionPlayer.getLastNodeInSelection(), true );\n\t\tconst startOffset = range.startOffset;\n\t\tconst endOffset = range.endOffset - 1;\n\n\t\tconst startUtterance = storage.getStartUtterance( startNode, startOffset );\n\t\tconst endUtterance = storage.getEndUtterance( endNode, endOffset );\n\n\t\tstorage.prepareUtterance( startUtterance ).done( () => {\n\t\t\tstorage.prepareUtterance( endUtterance ).done( () => {\n\t\t\t\tconst startToken = storage.getStartToken( startUtterance, startNode, startOffset );\n\t\t\t\tconst endToken = storage.getEndToken( endUtterance, endNode, endOffset );\n\n\t\t\t\tconst selectedText = selection.toString();\n\n\t\t\t\tif ( !selectedText.trim() || !/^[A-Za-zÅÄÖåäö]+$/.test( startToken.string ) ) {\n\t\t\t\t\tmw.notify( mw.msg( 'wikispeech-no-word-error' ), { type: 'warn' } );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif ( startToken !== endToken ) {\n\t\t\t\t\tmw.notify( mw.msg( 'wikispeech-one-word-only' ), { type: 'warn' } );\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tselectedWord = startToken.string;\n\t\t\t\tcontext = startToken.utterance.content.map( ( item ) => item.text ).join( ' ' );\n\t\t\t\topenDialog();\n\t\t\t} );\n\t\t} );\n\t} else {\n\t\tmw.notify( mw.msg( 'wikispeech-tip-highlight-word' ), { type: 'info' } );\n\t\topenDialog();\n\t}\n};\nfeedback.reportPronunciationError = reportPronunciationError;\nmodule.exports = feedback;\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.gadget.js","messages":[{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"mainInstance\" type.","line":33,"column":1,"nodeType":"Block","endLine":33,"endColumn":1},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":39,"column":3,"nodeType":"CallExpression","endLine":52,"endColumn":7}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Loads wikispeech modules from producer\n *\n * @module ext.wikispeech.gadget\n */\n\nlet moduleUrl, api, main;\n\nconst {\n\taddUserOptions\n} = require( './ext.wikispeech.sharedUserOptionSettings.js' );\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 * 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 *\n * @param mainInstance\n */\nfunction extendUi( mainInstance ) {\n\tif ( mw.config.get( 'wgWikispeechAllowConsumerEdits' ) ) {\n\t\tconst producerUrl = mw.config.get( 'wgWikispeechProducerUrl' );\n\t\tconst producerApi = new mw.ForeignApi( `${ producerUrl }/api.php` );\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\tconst consumerUrl = window.location.origin + mw.config.get( 'wgScriptPath' );\n\t\t\t\tconst editButtonItem = mainInstance.ui.createEditButton( scriptPath, consumerUrl );\n\t\t\t\tmainInstance.ui.addMenuItem( editButtonItem );\n\n\t\t\t} );\n\t}\n}\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] ).then( async () => {\n\tconst producerUrl = mw.config.get( 'wgWikispeechProducerUrl' );\n\tif ( !producerUrl ) {\n\t\tmw.log.error( '[Wikispeech] No producer URL given. Set it with the config variable \"wgWikispeechProducerUrl\".' );\n\t\treturn;\n\t}\n\n\taddConfig();\n\tapi = new mw.Api();\n\tawait addUserOptions( api, false );\n\tconst parametersString = $.param( {\n\t\tlang: mw.config.get( 'wgUserLanguage' ),\n\t\tskin: mw.config.get( 'skin' ),\n\t\traw: 1,\n\t\tsafemode: 1,\n\t\tmodules: 'ext.wikispeech'\n\t} );\n\tmoduleUrl = `${ producerUrl }/load.php?${ parametersString }`;\n\tmw.log( `[Wikispeech] Loading wikispeech module from ${ moduleUrl }` );\n\ttry {\n\t\tawait mw.loader.getScript( moduleUrl );\n\t\tawait mw.loader.using( 'ext.wikispeech' );\n\t\tmain = require( 'ext.wikispeech' );\n\t\tawait main.ui.ready;\n\t\tmain.ui.isProducer = false;\n\t\textendUi( main );\n\t} catch ( error ) {\n\t\tmw.log.error( '[Wikispeech] Failed to load Wikispeech module: ', error );\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.helpDialog.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/modules/ext.wikispeech.highlighter.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/modules/ext.wikispeech.loader.js","messages":[{"ruleId":"no-jquery/no-done-fail","severity":1,"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":0,"fatalErrorCount":0,"warningCount":1,"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":1,"message":"Prefer .then to .done","line":129,"column":1,"nodeType":"CallExpression","endLine":133,"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":64,"column":29,"nodeType":"CallExpression","endLine":64,"endColumn":60,"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"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 */\nconst Ui = require( './ext.wikispeech.ui.js' );\nconst Storage = require( './ext.wikispeech.storage.js' );\nconst Player = require( './ext.wikispeech.player.js' );\nconst SelectionPlayer = require( './ext.wikispeech.selectionPlayer.js' );\nconst Highlighter = require( './ext.wikispeech.highlighter.js' );\nconst { addUserOptions } = require( './ext.wikispeech.sharedUserOptionSettings.js' );\n\nclass Main {\n\tconstructor() {\n\t\tthis.storage = new Storage();\n\t\tthis.selectionPlayer = new SelectionPlayer();\n\t\tthis.ui = new Ui();\n\t\tthis.player = new Player();\n\t\tthis.highlighter = new Highlighter();\n\n\t\tthis.highlighter.storage = this.storage;\n\t\tthis.storage.player = this.player;\n\t\tthis.storage.highlighter = this.highlighter;\n\t\tthis.player.ui = this.ui;\n\t\tthis.player.storage = this.storage;\n\t\tthis.player.highlighter = this.highlighter;\n\t\tthis.player.selectionPlayer = this.selectionPlayer;\n\t\tthis.selectionPlayer.storage = this.storage;\n\t\tthis.selectionPlayer.player = this.player;\n\t\tthis.selectionPlayer.ui = this.ui;\n\t\tthis.selectionPlayer.highlighter = this.highlighter;\n\t\tthis.ui.player = this.player;\n\t\tthis.ui.storage = this.storage;\n\t\tthis.ui.selectionPlayer = this.selectionPlayer;\n\t}\n\n\tasync init() {\n\t\tif ( !this.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\tconst api = new mw.Api();\n\t\tawait addUserOptions( api, true );\n\n\t\tthis.storage.loadUtterances( window );\n\t\t// Prepare the first utterance for playback.\n\n\t\tthis.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( 'click', this.toggleVisibility );\n\n\t\tdocument.addEventListener(\n\t\t\t'mouseenter',\n\t\t\tthis.player.readUi.bind( this.player ),\n\t\t\ttrue\n\t\t);\n\n\t\tdocument.addEventListener(\n\t\t\t'focus',\n\t\t\tthis.player.readUi.bind( this.player ),\n\t\t\ttrue\n\t\t);\n\t}\n\n\t/**\n\t * Toggle the visibility of the control panel.\n\t *\n\t * @method\n\t * @memberof ext.wikispeech.Main\n\t * @param {Event} event\n\t */\n\n\ttoggleVisibility( event ) {\n\t\tthis.ui.toggleVisibility();\n\n\t\tlet toggleVisibilityMessage;\n\t\tif ( this.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 * @method\n\t * @memberof ext.wikispeech.Main\n\t * @return {boolean} true is the namespace of current page\n\t *  should activate Wikispeech, else false.\n\t */\n\n\tenabledForNamespace() {\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\nconst main = new Main();\nmain.ui.isProducer = true;\n\nmw.loader.using( [ 'mediawiki.api', 'ext.wikispeech' ] ).done(\n\tasync () => {\n\t\tawait main.init();\n\t}\n);\n\nmodule.exports = main;\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":1,"message":"Prefer .then to .done","line":246,"column":3,"nodeType":"CallExpression","endLine":251,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .fail","line":246,"column":3,"nodeType":"CallExpression","endLine":269,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":259,"column":5,"nodeType":"CallExpression","endLine":268,"endColumn":9}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Play, pause stop and navigate in the recitation.\n *\n * @class ext.wikispeech.Player\n * @constructor\n */\n\nconst util = require( './ext.wikispeech.util.js' );\n\nclass Player {\n\tconstructor() {\n\t\tthis.currentUtterance = null;\n\t\tthis.paused = false;\n\t\tthis.playingSelection = false;\n\n\t\tthis.ui = null;\n\t\tthis.storage = null;\n\t\tthis.highlighter = null;\n\t\tthis.selectionPlayer = null;\n\n\t\tthis.errorUtteranceList = null;\n\t\tthis.errorUtteranceIndex = 0;\n\t\tthis.errorAudio = null;\n\n\t\tthis.toolbarPlayer = new Audio();\n\t}\n\n\tasync readUi( e ) {\n\n\t\tif ( !mw.user.options.get( 'wikispeechPageToolbar' ) ) {\n\t\t\treturn;\n\t\t}\n\t\tconst inToolbar = e.target.closest( '.vector-page-toolbar' );\n\t\tif ( !inToolbar ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst toolbarLink = e.target.closest( '.vector-page-toolbar a' );\n\t\tlet dropdownLabel = e.target.closest( '.vector-dropdown-label' );\n\n\t\tif ( !dropdownLabel && e.target.matches( 'input.vector-dropdown-checkbox' ) ) {\n\t\t\tdropdownLabel = e.target.parentElement.querySelector( '.vector-dropdown-label' );\n\t\t}\n\n\t\tif ( !toolbarLink && !dropdownLabel ) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet text = '';\n\t\tif ( dropdownLabel ) {\n\t\t\ttext = dropdownLabel.querySelector( '.vector-dropdown-label-text' ).textContent;\n\t\t} else {\n\t\t\ttext = e.target.textContent;\n\t\t}\n\n\t\tconst lang = mw.config.get( 'wgPageContentLanguage' );\n\t\tconst utterance = await this.storage.getUiUtterance( lang, text );\n\n\t\tthis.toolbarPlayer.pause();\n\t\tthis.toolbarPlayer.currentTime = 0;\n\t\tthis.toolbarPlayer.src = utterance.audio.src;\n\t\tthis.toolbarPlayer.play();\n\n\t}\n\n\t/**\n\t * Play or pause, depending on whether an utterance is playing.\n\t */\n\n\tplayOrPause() {\n\t\tif ( this.isPlaying() && !this.paused ) {\n\t\t\tthis.pause();\n\t\t} else {\n\t\t\tthis.play();\n\t\t}\n\t}\n\n\t/**\n\t * Play or stop, depending on whether an utterance is playing.\n\t */\n\n\tplayOrStop() {\n\t\tif ( this.isPlaying() ) {\n\t\t\tthis.stop();\n\t\t} else {\n\t\t\tthis.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\tisPlaying() {\n\t\treturn this.currentUtterance !== null;\n\t}\n\n\t/**\n\t * Stop playing the utterance currently playing.\n\t */\n\n\tstop() {\n\n\t\tthis.ui.setAllPlayerIconsToPlay();\n\n\t\tthis.paused = false;\n\n\t\tif ( this.errorAudio ) {\n\t\t\tthis.errorAudio.pause();\n\t\t\tthis.errorAudio = null;\n\t\t}\n\n\t\tif ( this.isPlaying() ) {\n\t\t\tthis.stopUtterance( this.currentUtterance );\n\t\t\tthis.currentUtterance = null;\n\t\t}\n\n\t\tthis.ui.hideBufferingIcon();\n\n\t\tthis.playingSelection = false;\n\t}\n\n\t/**\n\t * Pause playing the utterance currently playing, and resume from paused utterance.\n\t */\n\tpause() {\n\t\tif ( this.isPlaying() && !this.paused ) {\n\t\t\tthis.paused = true;\n\t\t\tthis.pauseUtterance( this.currentUtterance );\n\t\t}\n\t\tif ( this.playingSelection ) {\n\t\t\tthis.stop();\n\t\t}\n\t\tthis.ui.setAllPlayerIconsToPlay();\n\t\tthis.ui.hideBufferingIcon();\n\t}\n\n\t/**\n\t * Start playing the first utterance or selected text, if any.\n\t */\n\tplay() {\n\t\tif ( this.playingSelection ) {\n\t\t\tthis.stop();\n\t\t} else {\n\t\t\tthis.ui.setPlayPauseIconToPause();\n\t\t}\n\n\t\tif ( this.paused ) {\n\t\t\tthis.currentUtterance.audio.play();\n\t\t\tthis.paused = false;\n\n\t\t\tconst currentToken = this.getCurrentToken();\n\t\t\tif ( currentToken ) {\n\t\t\t\tthis.highlighter.startTokenHighlighting( currentToken );\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif ( this.storage.loadFailed ) {\n\t\t\tconst errorUtterance = { messageKey: 'noarticletext', audio: new Audio() };\n\t\t\tthis.storage.prepareUtterance( errorUtterance ).then( () => {\n\t\t\t\tthis.errorUtteranceList = errorUtterance.errorUtterances;\n\t\t\t\tthis.errorUtteranceIndex = 0;\n\n\t\t\t\tthis.playCurrentErrorUtterance();\n\t\t\t} );\n\t\t\treturn;\n\t\t}\n\t\tthis.storage.utterancesLoaded.then( () => {\n\t\t\tif ( !this.selectionPlayer.playSelectionIfValid() ) {\n\t\t\t\tif ( this.ui.isSelectionPlayerShown() && this.selectionPlayer.getFocus() ) {\n\t\t\t\t\tthis.selectionPlayer.playFromFocus();\n\t\t\t\t} else {\n\t\t\t\t\tthis.playUtterance( this.storage.utterances[ 0 ] );\n\t\t\t\t}\n\t\t\t}\n\n\t\t} );\n\t}\n\n\tplayCurrentErrorUtterance() {\n\t\tif ( this.errorAudio ) {\n\t\t\tthis.errorAudio.onended = null;\n\t\t\tthis.errorAudio.pause();\n\t\t\tthis.errorAudio.currentTime = 0;\n\t\t}\n\t\tif (\n\t\t\tthis.errorUtteranceList &&\n\t\t\tthis.errorUtteranceIndex < this.errorUtteranceList.length\n\t\t) {\n\t\t\tconst u = this.errorUtteranceList[ this.errorUtteranceIndex ];\n\t\t\tthis.currentUtterance = u;\n\n\t\t\tthis.errorAudio = u.audio;\n\t\t\tu.audio.onended = () => {\n\t\t\t\tthis.errorUtteranceIndex++;\n\t\t\t\tif ( this.errorUtteranceIndex < this.errorUtteranceList.length ) {\n\t\t\t\t\tthis.playCurrentErrorUtterance();\n\t\t\t\t} else {\n\t\t\t\t\tthis.errorUtteranceList = null;\n\t\t\t\t\tthis.errorUtteranceIndex = 0;\n\t\t\t\t\tthis.stop();\n\t\t\t\t}\n\t\t\t};\n\t\t\tu.audio.currentTime = 0;\n\t\t\tu.audio.play();\n\t\t}\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\tplayUtterance( utterance, fromStart ) {\n\t\tfromStart = fromStart === undefined ? true : fromStart;\n\t\tif ( fromStart && this.isPlaying() ) {\n\t\t\tthis.stopUtterance( this.currentUtterance );\n\t\t}\n\t\tthis.currentUtterance = utterance;\n\t\tif ( !this.playingSelection ) {\n\t\t\tthis.highlighter.highlightUtterance( utterance );\n\t\t}\n\t\tthis.ui.showBufferingIconIfAudioIsLoading( utterance );\n\t\tthis.prepareAndPlayUtterance( utterance );\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\tprepareAndPlayUtterance( utterance ) {\n\t\tthis.storage.prepareUtterance( utterance )\n\t\t\t.done( () => {\n\t\t\t\tif ( utterance === this.currentUtterance && !this.paused ) {\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 !== this.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\tthis.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\tthis.stop();\n\t\t\t\t\t\t} else if ( data.action === 'retry' ) {\n\t\t\t\t\t\t\tthis.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\tstopUtterance( utterance ) {\n\t\tutterance.audio.pause();\n\t\t// Rewind audio for next time it plays.\n\t\tutterance.audio.currentTime = 0;\n\t\tthis.ui.removeCanPlayListener( $( utterance.audio ) );\n\t\tthis.highlighter.clearHighlighting();\n\t}\n\n\t/**\n\t * Pause the audio for an utterance.\n\t *\n\t * @param {Object} utterance The utterance to pause the audio\n\t *  for.\n\t */\n\n\tpauseUtterance( utterance ) {\n\t\tutterance.audio.pause();\n\t\tthis.highlighter.clearHighlightTokenTimer();\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\tskipAheadUtterance() {\n\t\tif ( this.errorUtteranceList ) {\n\t\t\tif ( this.errorUtteranceIndex < this.errorUtteranceList.length - 1 ) {\n\t\t\t\tthis.errorUtteranceIndex++;\n\t\t\t\tthis.playCurrentErrorUtterance();\n\t\t\t} else {\n\t\t\t\tthis.stop();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst nextUtterance =\n\t\t\tthis.storage.getNextUtterance( this.currentUtterance );\n\t\tif ( nextUtterance ) {\n\t\t\tthis.playUtterance( nextUtterance );\n\t\t} else {\n\t\t\tthis.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\tskipBackUtterance() {\n\t\tif ( this.errorUtteranceList ) {\n\t\t\tif ( this.errorUtteranceIndex > 0 ) {\n\t\t\t\tthis.errorUtteranceIndex--;\n\t\t\t\tthis.playCurrentErrorUtterance();\n\t\t\t} else {\n\t\t\t\tthis.errorAudio.currentTime = 0;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst rewindThreshold = mw.config.get(\n\t\t\t'wgWikispeechSkipBackRewindsThreshold'\n\t\t);\n\t\tconst time = this.currentUtterance.audio.currentTime;\n\t\tif (\n\t\t\ttime > rewindThreshold ||\n\t\t\t\tthis.currentUtterance === this.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\tthis.currentUtterance.audio.currentTime = 0;\n\t\t} else {\n\t\t\tconst previousUtterance =\n\t\t\t\tthis.storage.getPreviousUtterance(\n\t\t\t\t\tthis.currentUtterance\n\t\t\t\t);\n\t\t\tif ( previousUtterance ) {\n\t\t\t\tthis.playUtterance( previousUtterance );\n\t\t\t} else {\n\t\t\t\tthis.stop();\n\t\t\t}\n\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. Can return null.\n\t */\n\n\tgetCurrentToken() {\n\t\tif ( !this.currentUtterance || !this.currentUtterance.tokens ) {\n\t\t\treturn null;\n\t\t}\n\t\tlet currentToken = null;\n\t\tconst tokens = this.currentUtterance.tokens;\n\t\tconst currentTime = this.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\n\t\tif ( tokensWithDuration.length === 0 ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst lastTokenWithDuration =\n\t\t\tutil.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\tskipAheadToken() {\n\t\tif ( !this.isPlaying() ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentToken = this.getCurrentToken();\n\t\tconst nextToken = this.storage.getNextToken( currentToken );\n\n\t\tif ( !nextToken ) {\n\t\t\tthis.skipAheadUtterance();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.currentUtterance.audio.currentTime = nextToken.startTime / 1000;\n\n\t\tthis.highlighter.clearHighlighting();\n\t\tthis.highlighter.highlightUtterance( this.currentUtterance );\n\n\t\tif ( this.paused ) {\n\t\t\tthis.highlighter.highlightToken( nextToken );\n\n\t\t} else {\n\t\t\tthis.highlighter.startTokenHighlighting( nextToken );\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\tskipBackToken() {\n\t\tif ( this.isPlaying() ) {\n\t\t\tconst currentToken = this.getCurrentToken();\n\t\t\tlet previousToken = this.storage.getPreviousToken( currentToken );\n\n\t\t\tif ( !previousToken ) {\n\n\t\t\t\tthis.skipBackUtterance();\n\t\t\t\tpreviousToken = this.storage.getLastToken( this.currentUtterance );\n\t\t\t}\n\n\t\t\tif ( previousToken ) {\n\t\t\t\tthis.highlighter.clearHighlighting();\n\t\t\t\tthis.highlighter.highlightUtterance( this.currentUtterance );\n\t\t\t}\n\n\t\t\tif ( previousToken ) {\n\t\t\t\tthis.currentUtterance.audio.currentTime = previousToken.startTime / 1000;\n\n\t\t\t\tif ( this.paused ) {\n\t\t\t\t\tthis.highlighter.highlightToken( previousToken );\n\t\t\t\t} else {\n\t\t\t\t\tthis.highlighter.startTokenHighlighting( previousToken );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n}\n\nmodule.exports = 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":1,"message":"Prefer .then to .done","line":124,"column":3,"nodeType":"CallExpression","endLine":136,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":158,"column":3,"nodeType":"CallExpression","endLine":170,"endColumn":7}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"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\nclass SelectionPlayer {\n\tconstructor() {\n\t\tthis.previousEndUtterance = null;\n\t\tthis.ui = null;\n\t\tthis.storage = null;\n\t\tthis.highlighter = null;\n\t}\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\tplaySelectionIfValid() {\n\t\tif ( this.isSelectionValid() ) {\n\t\t\tthis.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\tisSelectionValid() {\n\t\tif ( !this.isTextSelected() ) {\n\t\t\treturn false;\n\t\t}\n\t\tconst firstNode = this.getFirstNodeInSelection();\n\t\tconst firstTextNode =\n\t\t\tthis.storage.getFirstTextNode( firstNode, true );\n\t\tconst lastNode = this.getLastNodeInSelection();\n\t\tconst lastTextNode =\n\t\t\tthis.storage.getLastTextNode( lastNode, true );\n\t\tif (\n\t\t\tthis.storage.isNodeInUtterance( firstTextNode ) &&\n\t\t\t\tthis.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\tisTextSelected() {\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\tgetFirstNodeInSelection() {\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\tplaySelection() {\n\t\tthis.player.playingSelection = true;\n\t\tconst selection = window.getSelection();\n\n\t\tconst [ startNode, startOffset, startUtterance ] =\n\t\t\tthis.getSelectionStart( selection );\n\n\t\tthis.player.currentUtterance = startUtterance;\n\t\tthis.storage.prepareUtterance(\n\t\t\tstartUtterance\n\t\t)\n\t\t\t.done( () => {\n\t\t\t\tconst startToken = this.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\tthis.setStartTime( startUtterance, startToken.startTime );\n\t\t\t\tthis.player.playUtterance( startUtterance, false );\n\t\t\t\tthis.ui.setSelectionPlayerIconToStop();\n\t\t\t} );\n\t\tthis.ui.showBufferingIconIfAudioIsLoading( startUtterance );\n\n\t\tconst endRange = selection.getRangeAt( selection.rangeCount - 1 );\n\t\tconst lastSelectionNode = this.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\tthis.storage.getLastTextNode(\n\t\t\t\tlastSelectionNode,\n\t\t\t\ttrue\n\t\t\t);\n\t\tconst endUtterance =\n\t\t\tthis.storage.getEndUtterance( endNode, endOffset );\n\t\tthis.previousEndUtterance = endUtterance;\n\t\tthis.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 = this.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\tthis.setEndTime( endUtterance, endToken.endTime );\n\t\t\t} );\n\t}\n\n\t/**\n\t * Get node, offset and utterance for start of the selection.\n\t *\n\t * @return {Array} Sequence of Text, number and Object. Empty array if start\n\t *   couldn't be calculated.\n\t */\n\n\tgetSelectionStart( selection ) {\n\t\tconst startRange = selection.getRangeAt( 0 );\n\t\tconst firstSelectionNode = this.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 = this.storage.getFirstTextNode(\n\t\t\tfirstSelectionNode,\n\t\t\ttrue\n\t\t);\n\t\tif ( !startNode ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst startUtterance = this.storage.getStartUtterance(\n\t\t\tstartNode,\n\t\t\tstartOffset\n\t\t);\n\n\t\treturn [ startNode, startOffset, startUtterance ];\n\t}\n\n\t/**\n\t * Get node, offset and utterance for the focus.\n\t *\n\t * @return {Array} Sequence of Text, number and Object. Empty array if there\n\t *  is no focus.\n\t */\n\n\tgetFocus() {\n\t\tconst selection = window.getSelection();\n\t\tif ( !selection.anchorNode ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.getSelectionStart( selection );\n\t}\n\n\t/**\n\t * Gets the token in focus.\n\t *\n\t * @param {Object} utterance\n\t * @param {Text} node\n\t * @param {number} offest\n\t * @return {Object} Token in focus.\n\t */\n\n\tasync getTokenInFocus( utterance, node, offset ) {\n\t\tawait this.storage.prepareUtterance( utterance );\n\t\tconst startToken = this.storage.getStartToken(\n\t\t\tutterance,\n\t\t\tnode,\n\t\t\toffset\n\t\t);\n\n\t\treturn startToken;\n\t}\n\n\t/**\n\t * Start playing from a token.\n\t *\n\t * @param {Object} utterance\n\t * @param {Object} token\n\t */\n\n\tplayFromToken( utterance, token ) {\n\t\tthis.player.playUtterance( utterance, token );\n\t\tthis.setStartTime( utterance, token.startTime );\n\t}\n\n\t/**\n\t * Start playing from a certain point in the text.\n\t */\n\n\tasync playFromFocus() {\n\t\t// Hide the selection player because the focus is lost if we click the\n\t\t// button.\n\t\tthis.ui.hideSelectionPlayer();\n\n\t\tconst [ node, offset, utterance ] = this.getFocus();\n\t\tthis.ui.showBufferingIconIfAudioIsLoading( utterance );\n\t\tconst token = await this.getTokenInFocus( utterance, node, offset );\n\t\tthis.playFromToken( utterance, token );\n\t\tthis.highlighter.startTokenHighlighting( token );\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\tsetStartTime( 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\tgetLastNodeInSelection() {\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\tsetEndTime( 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\tthis.player.stop();\n\t\t\t\t\t\tthis.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\tresetPreviousEndUtterance() {\n\t\tif ( this.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$( this.previousEndUtterance.audio ).off( 'playing.end' );\n\t\t\twindow.clearTimeout( this.previousEndUtterance.stopTimeout );\n\t\t\tthis.previousEndUtterance.stopTimeout = null;\n\t\t\tthis.previousEndUtterance = null;\n\t\t}\n\t}\n}\n\nmodule.exports = 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.sharedUserOptionSettings.js","messages":[{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"api\" type.","line":19,"column":1,"nodeType":"Block","endLine":19,"endColumn":1},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":31,"column":3,"nodeType":"CallExpression","endLine":50,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .fail","line":31,"column":3,"nodeType":"CallExpression","endLine":57,"endColumn":7},{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"api\" type.","line":69,"column":1,"nodeType":"Block","endLine":69,"endColumn":1},{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"isProducer\" type.","line":70,"column":1,"nodeType":"Block","endLine":70,"endColumn":1},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":89,"column":3,"nodeType":"CallExpression","endLine":102,"endColumn":6},{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"api\" type.","line":115,"column":1,"nodeType":"Block","endLine":115,"endColumn":1},{"ruleId":"jsdoc/no-undefined-types","severity":1,"message":"The type 'ext.wikispeech.UserOptionsDialog' is undefined.","line":116,"column":1,"nodeType":"Block","endLine":116,"endColumn":1},{"ruleId":"jsdoc/require-param-type","severity":1,"message":"Missing JSDoc @param \"isProducer\" type.","line":117,"column":1,"nodeType":"Block","endLine":117,"endColumn":1},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":133,"column":3,"nodeType":"CallExpression","endLine":141,"endColumn":7},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .fail","line":133,"column":3,"nodeType":"CallExpression","endLine":147,"endColumn":7}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":11,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * @module ext.wikispeech.sharedUserOptionSettings\n */\n\nconst defaultOptions = require( './default-user-options.json' );\n\nfunction computeOptionsPage() {\n\tconst namespace = mw.config.get( 'wgNamespaceIds' ).user;\n\tconst userPage = mw.Title.makeTitle( namespace, mw.user.getName() ).getPrefixedText();\n\treturn userPage + '/Wikispeech_preferences';\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 * @param api\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( api ) {\n\tconst optionsPage = computeOptionsPage();\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 * @param api\n * @param isProducer\n * @return {jQuery.Deferred} Resolves when user options have been\n *  read.\n */\nfunction addUserOptions( api, isProducer ) {\n\tconst done = $.Deferred();\n\tif ( isProducer ) {\n\t\tObject.keys( defaultOptions ).forEach( ( key ) => {\n\t\t\tconst value = mw.user.options.get( key );\n\t\t\tlet finalValue;\n\t\t\tif ( value !== undefined ) {\n\t\t\t\tfinalValue = value;\n\t\t\t} else {\n\t\t\t\tfinalValue = defaultOptions[ key ];\n\t\t\t}\n\t\t\tmw.user.options.set( key, finalValue );\n\t\t} );\n\t\tdone.resolve();\n\t} else {\n\t\tgetUserOptionsOnConsumer( api ).done( ( options ) => {\n\t\t\tObject.keys( defaultOptions ).forEach( ( key ) => {\n\t\t\t\tlet value;\n\t\t\t\t// Take the option value from user page if it is set,\n\t\t\t\t// otherwise use default.\n\t\t\t\tif ( Object.keys( options ).includes( key ) ) {\n\t\t\t\t\tvalue = options[ key ];\n\t\t\t\t} else {\n\t\t\t\t\tvalue = defaultOptions[ key ];\n\t\t\t\t}\n\t\t\t\tmw.user.options.set( key, value );\n\t\t\t} );\n\t\t\tdone.resolve();\n\t\t} );\n\t}\n\n\treturn done;\n}\n\n/**\n * Write user options to Special:Preferences, and a subpage to the user page\n * on the gadget solution if on producer mode.\n *\n * User options are read from the preferences popup dialog and\n * stored as JSON on the user page and as values in user preferences.\n *\n * @param api\n * @param {ext.wikispeech.UserOptionsDialog} dialog\n * @param isProducer\n */\nfunction writeUserOptionsPreferences( api, dialog, isProducer ) {\n\tconst optionsPage = computeOptionsPage();\n\tconst options = Object.assign( {}, defaultOptions );\n\tconst voice = dialog.getVoice();\n\toptions[ voice.variable ] = voice.voice;\n\toptions.wikispeechSpeechRate = dialog.getSpeechRate();\n\toptions.wikispeechPartOfContent = dialog.getPartOfContent() ? '1' : '0';\n\n\tapi.saveOption( voice.variable, voice.voice );\n\tapi.saveOption( 'wikispeechSpeechRate', String( options.wikispeechSpeechRate ) );\n\tapi.saveOption( 'wikispeechPartOfContent', options.wikispeechPartOfContent );\n\n\tif ( !isProducer ) {\n\t\tconst optionsJson = JSON.stringify( options, null, 4 );\n\t\tapi.postWithEditToken( {\n\t\t\taction: 'edit',\n\t\t\ttitle: optionsPage,\n\t\t\ttext: optionsJson,\n\t\t\tformatversion: 2\n\t\t} )\n\t\t\t.done( () => {\n\t\t\t\tmw.log( '[Wikispeech] Wrote user preferences to \"' + optionsPage + '\".' );\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 write user preferences to \"' +\n\t\t\t\t\t\toptionsPage + '\": ' + error\n\t\t\t\t);\n\t\t\t} );\n\t}\n}\n\nmodule.exports = {\n\tcomputeOptionsPage,\n\tgetUserOptionsOnConsumer,\n\taddUserOptions,\n\twriteUserOptionsPreferences\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.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":5,"column":19,"nodeType":"CallExpression","endLine":5,"endColumn":42,"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":1,"message":"Prefer .then to .done","line":259,"column":19,"nodeType":"CallExpression","endLine":271,"endColumn":7}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"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\nconst util = require( './ext.wikispeech.util.js' );\n\nclass Storage {\n\tconstructor() {\n\t\tthis.utterances = [];\n\t\tthis.utterancesLoaded = $.Deferred();\n\t\tthis.loadFailed = false;\n\t\tthis.uiUtterances = {};\n\n\t\tconst producerUrl = mw.config.get( 'wgWikispeechProducerUrl' );\n\t\tif ( producerUrl ) {\n\t\t\tconst producerApiUrl = `${ producerUrl }/api.php`;\n\t\t\tthis.api = new mw.ForeignApi( producerApiUrl );\n\t\t} else {\n\t\t\tthis.api = new mw.Api();\n\t\t}\n\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\tloadUtterances( 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\t'part-of-content': mw.user.options.get( 'wikispeechPartOfContent' )\n\t\t};\n\t\tif ( mw.config.get( 'wgWikispeechProducerUrl' ) ) {\n\t\t\toptions[ 'consumer-url' ] = window.location.origin +\n\t\t\t\tmw.config.get( 'wgScriptPath' );\n\t\t}\n\t\tthis.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).then( ( data ) => {\n\t\t\tmw.log( 'Segments received:', data );\n\t\t\tthis.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 = this.utterances[ 0 ];\n\t\t\tconst firstNode = this.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 < this.utterances.length; i++ ) {\n\t\t\t\tconst utterance = this.utterances[ i ];\n\t\t\t\tutterance.audio = $( '<audio>' ).get( 0 );\n\t\t\t}\n\t\t\tthis.utterancesLoaded.resolve();\n\t\t\tthis.prepareUtterance( this.utterances[ 0 ] );\n\t\t} ).catch( ( jqXHR ) => {\n\t\t\tif ( jqXHR === 'missingtitle' ) {\n\t\t\t\tthis.loadFailed = true;\n\t\t\t}\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\tprepareUtterance( 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!this.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\tthis.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 = this.getNextUtterance( utterance );\n\t\t\tif ( nextUtterance ) {\n\t\t\t\t$audio.on( {\n\t\t\t\t\tplay: () => {\n\t\t\t\t\t\tthis.prepareUtterance( nextUtterance );\n\t\t\t\t\t},\n\t\t\t\t\tended: () => {\n\t\t\t\t\t\tthis.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\tthis.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 = this.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\tloadAudio( utterance ) {\n\t\tconst utteranceIndex = this.utterances.includes( utterance ) ?\n\t\t\tthis.utterances.indexOf( utterance ) : 'message';\n\n\t\tmw.log(\n\t\t\t'Loading audio for utterance #' + utteranceIndex + ':',\n\t\t\tutterance\n\t\t);\n\t\tif ( utterance.messageKey ) {\n\t\t\treturn this.requestMessageUtteranceTts( utterance.messageKey )\n\t\t\t\t.then( ( response ) => {\n\t\t\t\t\tconst tts = response[ 'wikispeech-listen' ];\n\t\t\t\t\tconst utterances = tts.utterances;\n\n\t\t\t\t\tif ( !utterances || utterances.length === 0 ) {\n\t\t\t\t\t\tmw.log.error( 'No utterances for message key', utterance.messageKey );\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tutterance.errorUtterances = this.createErrorUtterances( utterance, utterances );\n\t\t\t\t\tthis.prepareUtterance( utterance.errorUtterances );\n\t\t\t\t} );\n\t\t} else {\n\t\t\treturn this.requestTts( utterance.hash, window )\n\t\t\t\t.then( ( response ) => {\n\t\t\t\t\tconst audioBase64 = response[ 'wikispeech-listen' ].audio;\n\t\t\t\t\tthis.setAudio( utterance.audio, audioBase64 );\n\n\t\t\t\t\tmw.log(\n\t\t\t\t\t\t'Setting audio url for: [' + utteranceIndex + ']',\n\t\t\t\t\t\tutterance, '=',\n\t\t\t\t\t\taudioBase64.length + ' base64 bytes'\n\t\t\t\t\t);\n\n\t\t\t\t\tthis.addTokens(\n\t\t\t\t\t\tutterance,\n\t\t\t\t\t\tresponse[ 'wikispeech-listen' ].tokens\n\t\t\t\t\t);\n\t\t\t\t} );\n\t\t}\n\n\t}\n\n\t/**\n\t * Set audio source and playback rate.\n\t *\n\t * @param {HTMLAudioElement} audioElement\n\t * @param {string} audioBase64\n\t */\n\tsetAudio( audioElement, audioBase64 ) {\n\t\taudioElement.src = 'data:audio/ogg;base64,' + audioBase64;\n\t\taudioElement.playbackRate = mw.user.options.get( 'wikispeechSpeechRate' );\n\t}\n\n\t/**\n\t * Create error utterance objects from TTS response.\n\t *\n\t * Each error utterance gets its own audio element and tokens,\n\t * so it can be played andd navigated independently.\n\t *\n\t * @param {Object} baseUtterance\n\t * @param {Array} utterances\n\t *@return {Array} List of utterance objects.\n\t */\n\tcreateErrorUtterances( baseUtterance, utterances ) {\n\t\treturn utterances.map( ( res ) => {\n\t\t\tconst u = Object.assign( {}, baseUtterance, {\n\t\t\t\taudio: new Audio(),\n\t\t\t\ttokens: res.tokens,\n\t\t\t\tmessageKey: baseUtterance.messageKey\n\t\t\t} );\n\t\t\tthis.setAudio( u.audio, res.audio );\n\t\t\tlet prevEnd = 0;\n\t\t\tu.tokens = res.tokens.map( ( t ) => {\n\t\t\t\tconst token = Object.assign( {}, t, {\n\t\t\t\t\tstring: t.orth,\n\t\t\t\t\tstartTime: prevEnd,\n\t\t\t\t\tendTime: t.endtime,\n\t\t\t\t\tutterance: u,\n\t\t\t\t\tisErrorUtterance: true\n\t\t\t\t} );\n\t\t\t\tprevEnd = t.endtime;\n\t\t\t\treturn token;\n\t\t\t} );\n\t\t\treturn u;\n\t\t} );\n\t}\n\n\t/**\n\t * Request is sent via the \"wikispeech-listen\" API action.\n\t *\n\t * @param {Object} options\n\t * @param {Object} window\n\t * @return {jQuery.Promise}\n\t */\n\trequestListen( options, window ) {\n\t\tif ( mw.config.get( 'wgWikispeechProducerUrl' ) ) {\n\t\t\toptions[ 'consumer-url' ] = window.location.origin +\n\t\t\t\tmw.config.get( 'wgScriptPath' );\n\t\t}\n\t\tconst request = this.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 * Send a page-specific request to the Speechoid service.\n\t *\n\t * Language to 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\trequestTts( segmentHash, window ) {\n\t\tconst language = mw.config.get( 'wgPageContentLanguage' );\n\t\tconst voice = util.getUserVoice( language );\n\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\n\t\tif ( voice !== '' ) {\n\t\t\toptions.voice = voice;\n\t\t}\n\n\t\treturn this.requestListen( options, window );\n\t}\n\n\t/**\n\t * Send a messageKey specific request to the Speechoid service.\n\t *\n\t * @param {string} messageText\n\t * @return {jQuery.Promise}\n\t */\n\n\trequestMessageUtteranceTts( messageText ) {\n\t\tconst language = mw.config.get( 'wgPageContentLanguage' );\n\n\t\tconst options = {\n\t\t\taction: 'wikispeech-listen',\n\t\t\tformat: 'json',\n\t\t\tlang: language,\n\t\t\t'message-key': messageText\n\t\t};\n\n\t\treturn this.requestListen( options );\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\taddTokens( utterance, responseTokens ) {\n\t\tif ( !utterance.content ) {\n\t\t\tutterance.tokens = responseTokens;\n\t\t\treturn;\n\t\t}\n\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 = this.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\taddOffsetsAndItems(\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\tthis.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\tutil.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\tutil.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\tgetStartOffsetInUtteranceString(\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\tgetNextUtterance( utterance ) {\n\t\treturn this.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\tgetUtteranceByOffset( utterance, offset ) {\n\t\tif ( utterance === null ) {\n\t\t\treturn null;\n\t\t}\n\t\tconst index = this.utterances.indexOf( utterance );\n\t\treturn this.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\tgetPreviousUtterance( utterance ) {\n\t\treturn this.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\tgetNextToken( 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 ) => !this.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\tisSilent( 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\tgetPreviousToken( 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 ) => !this.isSilent( token ) );\n\t\tif ( precedingTokens.length === 0 ) {\n\t\t\treturn null;\n\t\t} else {\n\t\t\tconst previousToken = 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\tgetLastToken( utterance ) {\n\t\tconst nonSilentTokens = utterance.tokens.filter( ( token ) => !this.isSilent( token ) );\n\t\tconst lastToken = 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\tgetFirstTextNode( node, inUtterance ) {\n\t\tif ( node.nodeType === 3 ) {\n\t\t\tif ( !inUtterance || this.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 = this.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\tisNodeInUtterance( node ) {\n\t\tfor ( let i = 0; i < this.utterances.length; i++ ) {\n\t\t\tconst utterance = this.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 ( this.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\tgetStartUtterance( node, offset ) {\n\t\tfor ( ; offset < node.textContent.length; offset++ ) {\n\t\t\tfor ( let i = 0; i < this.utterances.length; i++ ) {\n\t\t\t\tconst utterance = this.utterances[ i ];\n\t\t\t\tif (\n\t\t\t\t\tthis.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 = this.getNextTextNode( node );\n\t\treturn this.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\tisPointInItems(\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\tthis.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 ( this.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\tgetNextTextNode( 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 this.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 = this.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 this.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\tgetStartToken( 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\tthis.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 = this.getNextTextNode( node );\n\t\treturn this.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\tgetLastTextNode( node, inUtterance ) {\n\t\tif ( node.nodeType === 3 ) {\n\t\t\tif ( !inUtterance || this.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 = this.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\tgetEndUtterance( node, offset ) {\n\t\tfor ( ; offset >= 0; offset-- ) {\n\t\t\tfor ( let i = 0; i < this.utterances.length; i++ ) {\n\t\t\t\tconst utterance = this.utterances[ i ];\n\t\t\t\tif (\n\t\t\t\t\tthis.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 = this.getPreviousTextNode( node );\n\t\treturn this.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\tgetPreviousTextNode( node ) {\n\t\tconst previousNode = node.previousSibling;\n\t\tif ( previousNode === null ) {\n\t\t\treturn this.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 = this.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 this.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\tgetEndToken( 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\tthis.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 = this.getPreviousTextNode( node );\n\t\treturn this.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\tgetNodeForItem( item ) {\n\t\tif ( item.path === null ) {\n\t\t\treturn null;\n\t\t}\n\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\tasync getUiUtterance( lang, text ) {\n\t\tconst key = lang + ' | ' + text;\n\t\tif ( this.uiUtterances[ key ] ) {\n\t\t\treturn this.uiUtterances[ key ];\n\t\t}\n\n\t\tconst options = {\n\t\t\taction: 'wikispeech-listen',\n\t\t\tlang: lang,\n\t\t\ttext: text\n\t\t};\n\n\t\tconst voice = util.getUserVoice( lang );\n\t\tif ( voice !== '' ) {\n\t\t\toptions.voice = voice;\n\t\t}\n\t\tif ( mw.config.get( 'wgWikispeechProducerUrl' ) ) {\n\t\t\toptions[ 'consumer-url' ] = window.location.origin +\n\t\t\t\t\tmw.config.get( 'wgScriptPath' );\n\t\t}\n\n\t\tconst data = await this.api.get( options );\n\t\tconst utterance = {\n\t\t\taudio: new Audio(\n\t\t\t\t'data:audio/ogg;base64,' + data[ 'wikispeech-listen' ].audio\n\t\t\t),\n\t\t\ttokens: data[ 'wikispeech-listen' ].tokens\n\t\t};\n\n\t\tthis.uiUtterances[ key ] = utterance;\n\t\treturn utterance;\n\t}\n\n}\n\nmodule.exports = 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":"max-len","severity":1,"message":"This line has a length of 103. Maximum allowed is 100.","line":37,"column":1,"nodeType":"Program","messageId":"max","endLine":37,"endColumn":98},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":66,"column":19,"nodeType":"CallExpression","endLine":74,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .fail","line":66,"column":19,"nodeType":"CallExpression","endLine":80,"endColumn":6}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"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 */\nclass TranscriptionPreviewer {\n\tconstructor(\n\t\t$language,\n\t\t$transcription,\n\t\tapi,\n\t\t$player\n\t) {\n\t\tthis.$language = $language;\n\t\tthis.$transcription = $transcription;\n\t\tthis.api = api;\n\t\tthis.$player = $player;\n\t\tthis.lastTranscription = null;\n\t}\n\n\t/**\n\t * Play the transcription using TTS.\n\t *\n\t * If the transcription has changed since last play, a new one\n\t * retrieved. Otherwise the previous one is replayed.\n\t *\n\t * @return {jQuery.Promise} Fulfilled when audio starts playing, rejected if\n\t *  audio was not present.\n\t */\n\tplay() {\n\t\tconst transcription = this.$transcription.val();\n\t\t// Rewind in case it is already playing. Just calling play() is not enought to play from start.\n\t\tthis.$player.prop( 'currentTime', 0 );\n\n\t\tlet promise;\n\t\tif ( transcription !== this.lastTranscription || !this.$player.attr( 'src' ) ) {\n\t\t\tpromise = this.fetchAudio().then( () => {\n\t\t\t\tthis.$player.get( 0 ).play();\n\t\t\t} );\n\t\t\tthis.lastTranscription = transcription;\n\t\t} else {\n\t\t\tthis.$player.get( 0 ).play();\n\t\t\tpromise = $.Deferred().resolve().promise();\n\t\t}\n\n\t\treturn promise;\n\t}\n\n\t/**\n\t * Get audio for the player using the listen API\n\t *\n\t * @return {jQuery.Promise} Fulfilled when audio is fetched, rejected\n\t *  if there was an error.\n\t */\n\tfetchAudio() {\n\t\tconst language = this.$language.val();\n\t\tconst voice = util.getUserVoice( language );\n\t\tconst transcription = this.$transcription.val();\n\t\tmw.log( 'Fetching transcription preview for (' + language + '): ' + transcription );\n\n\t\tconst request = this.api.get( {\n\t\t\taction: 'wikispeech-listen',\n\t\t\tlang: language,\n\t\t\tipa: transcription,\n\t\t\tvoice: voice\n\t\t} ).done( ( response ) => {\n\t\t\tconst audioData = response[ 'wikispeech-listen' ].audio;\n\t\t\tthis.$player.attr( 'src', 'data:audio/ogg;base64,' + audioData );\n\t\t} ).fail( ( code, result ) => {\n\t\t\tthis.$player.attr( 'src', '' );\n\t\t\tmw.log.error( 'Failed to synthesize:', code, result );\n\t\t\tconst message = result.error.info;\n\t\t\tconst title = mw.msg( 'wikispeech-error-generate-preview-title' );\n\t\t\tOO.ui.alert( message, { title: title, size: 'medium' } );\n\t\t} );\n\n\t\treturn request;\n\t}\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":"mediawiki/no-unlabeled-buttonwidget","severity":1,"message":"OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true.","line":68,"column":18,"nodeType":"NewExpression","messageId":"noLabel","endLine":70,"endColumn":6},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":199,"column":3,"nodeType":"CallExpression","endLine":205,"endColumn":7},{"ruleId":"max-len","severity":1,"message":"This line has a length of 107. Maximum allowed is 100.","line":249,"column":1,"nodeType":"Program","messageId":"max","endLine":249,"endColumn":105},{"ruleId":"max-len","severity":1,"message":"This line has a length of 116. Maximum allowed is 100.","line":331,"column":1,"nodeType":"Program","messageId":"max","endLine":331,"endColumn":111},{"ruleId":"mediawiki/no-unlabeled-buttonwidget","severity":1,"message":"OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true.","line":428,"column":30,"nodeType":"NewExpression","messageId":"noLabel","endLine":431,"endColumn":6},{"ruleId":"mediawiki/no-unlabeled-buttonwidget","severity":1,"message":"OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true.","line":436,"column":23,"nodeType":"NewExpression","messageId":"noLabel","endLine":439,"endColumn":6},{"ruleId":"security/detect-non-literal-require","severity":1,"message":"Found non-literal argument in require","line":742,"column":21,"nodeType":"CallExpression","endLine":742,"endColumn":62}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":7,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Creates and controls the UI for the extension.\n *\n * @class ext.wikispeech.Ui\n * @constructor\n */\nconst util = require( './ext.wikispeech.util.js' );\nconst {\n\twriteUserOptionsPreferences\n} = require( './ext.wikispeech.sharedUserOptionSettings.js' );\nconst UserOptionsDialog = require( './ext.wikispeech.userOptionsDialog.js' );\nconst feedback = require( './ext.wikispeech.feedback.js' );\n\nclass Ui {\n\tconstructor() {\n\t\t// Resolves the UI is ready to be extended by consumer.\n\t\tthis.ready = $.Deferred();\n\t\tthis.player = null;\n\t\tthis.storage = null;\n\t\tthis.highlighter = null;\n\t\tthis.selectionPlayer = null;\n\t}\n\n\t/**\n\t * Initialize elements and functionality for the UI.\n\t */\n\n\tinit() {\n\t\tthis.addSelectionPlayer();\n\t\tthis.addControlPanel();\n\t\tthis.addKeyboardShortcuts();\n\t\tthis.windowManager = new OO.ui.WindowManager();\n\t\tthis.addDialogs();\n\t\tthis.loadErrorAudio();\n\t\tthis.ready.resolve();\n\t}\n\n\t/**\n\t * Adds an item to the link dropdown menu.\n\t *\n\t * @param {Object} item The menu item to add.\n\t * @param {string} item.label The label for the menu item.\n\t * @param {string} item.icon The icon for the menu item.\n\t */\n\taddMenuItem( item ) {\n\t\tconst option = new OO.ui.MenuOptionWidget( {\n\t\t\tdata: item,\n\t\t\tlabel: item.label,\n\t\t\ticon: item.icon\n\t\t} );\n\t\toption.$element\n\t\t\t.attr( 'title', item.label )\n\t\t\t.attr( 'aria-label', item.label );\n\n\t\tthis.linkMenuButton.getMenu().addItems( [ option ] );\n\t}\n\n\t/**\n\t * Adds a dropdown menu button to a toolbar group.\n\t *\n\t * @param {OO.ui.ToolGroup} group The group to add the menu button to.\n\t * @param {Function} onChoose Callback when a menu option is chosen.\n\t * @return {OO.ui.ButtonMenuSelectWidget} The created menu button.\n\t */\n\taddMenuButton( group, onChoose ) {\n\t\tconst label = mw.msg( 'wikispeech-menu-label' );\n\n\t\tconst button = new OO.ui.ButtonMenuSelectWidget( {\n\t\t\ticon: 'ellipsis'\n\t\t} );\n\n\t\tbutton.$element.find( 'a' )\n\t\t\t.attr( 'title', label )\n\t\t\t.attr( 'aria-label', label );\n\n\t\tbutton.getMenu().on( 'choose', ( item ) => {\n\t\t\tonChoose( item.getData(), item );\n\t\t} );\n\n\t\tgroup.addItems( [ button ] );\n\t\treturn button;\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\taddControlPanel() {\n\t\tconst toolFactory = new OO.ui.ToolFactory();\n\t\tconst toolGroupFactory = new OO.ui.ToolGroupFactory();\n\t\tthis.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\tthis.toolbar.$element\n\t\t\t.attr( 'aria-label', mw.msg( 'wikispeech-region-player' ) )\n\t\t\t.attr( 'role', 'region' );\n\n\t\tconst playerGroupPlayStop = this.addToolbarGroup();\n\t\tthis.playPauseButton = this.addButton(\n\t\t\tplayerGroupPlayStop,\n\t\t\t() => this.player.playOrPause(),\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'wikispeech-play' ),\n\t\t\t\ticon: 'play',\n\t\t\t\tflags: [\n\t\t\t\t\t'primary',\n\t\t\t\t\t'progressive'\n\t\t\t\t]\n\t\t\t}\n\t\t);\n\t\tthis.addButton(\n\t\t\tplayerGroupPlayStop,\n\t\t\t() => this.player.stop(),\n\t\t\t// TODO: add destructive flag when\n\t\t\t// https://gerrit.wikimedia.org/r/c/1194133 is done.\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'wikispeech-stop' ),\n\t\t\t\ticon: 'stop'\n\t\t\t}\n\t\t);\n\t\tconst playerGroup = this.addToolbarGroup();\n\t\tthis.addButton(\n\t\t\tplayerGroup,\n\t\t\t() => this.player.skipBackUtterance(),\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'wikispeech-skip-back' ),\n\t\t\t\ticon: 'doubleChevronStart'\n\t\t\t}\n\t\t);\n\t\tthis.addButton(\n\t\t\tplayerGroup,\n\t\t\t() => this.player.skipBackToken(),\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'wikispeech-previous' ),\n\t\t\t\ticon: 'previous'\n\t\t\t}\n\t\t);\n\t\tthis.addButton(\n\t\t\tplayerGroup,\n\t\t\t() => this.player.skipAheadToken(),\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'wikispeech-next' ),\n\t\t\t\ticon: 'next'\n\t\t\t}\n\t\t);\n\t\tthis.addButton(\n\t\t\tplayerGroup,\n\t\t\t() => this.player.skipAheadUtterance(),\n\t\t\t{\n\t\t\t\ttitle: mw.msg( 'wikispeech-skip-ahead' ),\n\t\t\t\ticon: 'doubleChevronEnd'\n\t\t\t}\n\t\t);\n\n\t\tthis.linkGroup = this.addToolbarGroup();\n\t\tthis.linkMenuButton = this.addMenuButton(\n\t\t\tthis.linkGroup,\n\t\t\t( data ) => {\n\t\t\t\tif ( data.click ) {\n\t\t\t\t\tdata.click();\n\t\t\t\t} else if ( data.url ) {\n\t\t\t\t\tif ( data.url === 'help' ) {\n\t\t\t\t\t\tconst help = require( './ext.wikispeech.helpDialog.js' );\n\t\t\t\t\t\thelp.openHelpDialog();\n\t\t\t\t\t} else {\n\t\t\t\t\t\twindow.open( data.url, '_blank' );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t);\n\t\tthis.addMenuItem( {\n\t\t\turl: 'help',\n\t\t\tlabel: mw.msg( 'wikispeech-help' ),\n\t\t\ticon: 'help'\n\t\t} );\n\t\tthis.addMenuItem( {\n\t\t\tclick: () => {\n\t\t\t\tfeedback.openPronunciationErrorDialog( {\n\t\t\t\t\tui: this,\n\t\t\t\t\tstorage: this.storage,\n\t\t\t\t\tselectionPlayer: this.selectionPlayer\n\t\t\t\t} );\n\t\t\t},\n\t\t\tlabel: mw.msg( 'wikispeech-feedback' ),\n\t\t\ticon: 'feedback'\n\t\t} );\n\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\tthis.addMenuItem( this.createEditButton( null, null ) );\n\t\t\t\t}\n\t\t\t} );\n\n\t\tif ( !mw.user.isAnon() ) {\n\t\t\tthis.addButton(\n\t\t\t\tthis.linkGroup,\n\t\t\t\tasync () => {\n\t\t\t\t\tconst data = await this.openWindow( this.settingsDialog );\n\t\t\t\t\tif ( data && data.action === 'save' ) {\n\t\t\t\t\t\twriteUserOptionsPreferences( api, this.settingsDialog, this.isProducer );\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle: mw.msg( 'wikispeech-settings' ),\n\t\t\t\t\ticon: 'settings'\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\t$( document.body ).append( this.toolbar.$element );\n\t\tthis.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 = this.toolbar.$element.height();\n\t\tthis.$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\tthis.addBufferingIcon();\n\t}\n\n\t/**\n\t * Add button that takes the user to the lexicon editor.\n\t *\n\t *  If`scriptUrl` is 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\t *  @param {string} [scriptUrl] Optional. The script endpoint for the editor page (i.e \"...index.php\").\n\t *  @param {string} [consumerUrl] Optional. The consumer URL to include as a parameter.\n\t *  @return {Object} Menu item configuration for the lexicon editor.\n\t */\n\n\tcreateEditButton( scriptUrl, consumerUrl ) {\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\tconst params = {\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\n\t\tif ( consumerUrl ) {\n\t\t\tparams.consumerUrl = consumerUrl;\n\t\t}\n\n\t\teditUrl += '?' + new URLSearchParams( params );\n\n\t\tconst label = mw.msg( 'wikispeech-edit-lexicon-btn' );\n\n\t\treturn {\n\t\t\turl: editUrl,\n\t\t\tlabel: label,\n\t\t\ticon: 'edit',\n\t\t\tid: '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\taddToolbarGroup() {\n\t\tconst group = new OO.ui.ButtonGroupWidget();\n\t\tthis.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 {Function|string} onClick Function to call or link.\n\t * @param {string} label Labels, such as aria labels and titles\n\t * @param {Object} config Configuration for the button widget.\n\t *  `title` is also used as `aria-label` attribute.\n\t *  See {@link OO.ui.ButtonWidget}.\n\t * @return {OO.ui.ButtonWidget}\n\t */\n\n\taddButton( group, onClick, config ) {\n\t\tconfig = config || {};\n\t\tconst button = new OO.ui.ButtonWidget( config );\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 ( config.title ) {\n\t\t\tbutton.$element.find( 'a' ).attr( 'aria-label', config.title );\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/pause button.\n\t *\n\t * The icon shows when the waiting for audio to play.\n\t */\n\n\taddBufferingIcon() {\n\t\tconst $playPauseButtons = $().add( this.playPauseButton.$element ).add( this.playSelectionButton.$element );\n\t\tconst $containers = $( '<span>' )\n\t\t\t.addClass( 'ext-wikispeech-buffering-icon-container' )\n\t\t\t.appendTo( ( $playPauseButtons ).find( '.oo-ui-iconElement-icon' ) );\n\t\tthis.$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\thideBufferingIcon() {\n\t\tthis.$bufferingIcons.hide();\n\t}\n\n\t/**\n\t * Show the buffering icon if the current audio is loading.\n\t *\n\t * @param {Object} utterance\n\t */\n\n\tshowBufferingIconIfAudioIsLoading( utterance ) {\n\t\tif ( this.audioIsReady( utterance.audio ) ) {\n\t\t\tthis.hideBufferingIcon();\n\t\t} else {\n\t\t\t$( utterance.audio ).on( 'canplay', () => {\n\t\t\t\tthis.hideBufferingIcon();\n\t\t\t} );\n\t\t\tthis.$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\taudioIsReady( 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\tremoveCanPlayListener( $audioElement ) {\n\t\t$audioElement.off( 'canplay' );\n\t}\n\n\t/**\n\t * Change the icon of the play/pause button to pause.\n\t */\n\n\tsetPlayPauseIconToPause() {\n\t\tthis.playPauseButton.setIcon( 'pause' );\n\t\tthis.playPauseButton.setTitle( mw.msg( 'wikispeech-pause' ) );\n\t\tthis.playPauseButton.$element.find( 'a' ).attr( 'aria-label', mw.msg( 'wikispeech-pause' ) );\n\t}\n\n\t/**\n\t * Change the icon of the play/pause button to play.\n\t */\n\n\tsetAllPlayerIconsToPlay() {\n\t\tthis.playPauseButton.setIcon( 'play' );\n\t\tthis.playPauseButton.setTitle( mw.msg( 'wikispeech-play' ) );\n\t\tthis.playPauseButton.$element.find( 'a' ).attr( 'aria-label', mw.msg( 'wikispeech-play' ) );\n\t\tthis.playSelectionButton.setIcon( 'play' );\n\t}\n\n\t/**\n\t * Change the icon of the selectionPlayer to stop.\n\t */\n\n\tsetSelectionPlayerIconToStop() {\n\t\tthis.playSelectionButton.setIcon( 'stop' );\n\t\tthis.playSelectionButton.setTitle( mw.msg( 'wikispeech-stop' ) );\n\t\tthis.playSelectionButton.$element.find( 'a' ).attr( 'aria-label', mw.msg( 'wikispeech-stop' ) );\n\t}\n\n\t/**\n\t * Add a small player that appears when text is selected.\n\t */\n\n\taddSelectionPlayer() {\n\t\tconst playLabel = mw.msg( 'wikispeech-play-selection' );\n\t\tthis.playSelectionButton = new OO.ui.ButtonWidget( {\n\t\t\ticon: 'play',\n\t\t\ttitle: playLabel\n\t\t} );\n\t\tthis.playSelectionButton.$element.find( 'a' ).attr( 'aria-label', playLabel );\n\t\tthis.playSelectionButton.on( 'click', () => this.player.playOrStop() );\n\n\t\tconst closeLabel = mw.msg( 'wikispeech-close-selection-player' );\n\t\tconst closeButton = new OO.ui.ButtonWidget( {\n\t\t\ticon: 'close',\n\t\t\ttitle: closeLabel\n\t\t} );\n\t\tcloseButton.$element.find( 'a' ).attr( 'aria-label', closeLabel );\n\t\tcloseButton.on( 'click', () => {\n\t\t\tthis.hideSelectionPlayer();\n\t\t\tthis.player.stop();\n\t\t} );\n\n\t\tthis.selectionPlayerUi = new OO.ui.ButtonGroupWidget( {\n\t\t\titems: [ this.playSelectionButton, closeButton ],\n\t\t\tclasses: [ 'ext-wikispeech-selection-player' ]\n\t\t} );\n\t\t$( document.body ).append( this.selectionPlayerUi.$element );\n\t\tthis.hideSelectionPlayer();\n\n\t\tconst pageContent = document.querySelector( mw.config.get( 'wgWikispeechContentSelector' ) );\n\t\t$( document ).on( 'mouseup', ( event ) => {\n\t\t\tif (\n\t\t\t\tthis.selectionPlayerUi.$element.get( 0 ).contains( event.target ) ||\n\t\t\t\tthis.playPauseButton.$element.get( 0 ).contains( event.target )\n\t\t\t) {\n\t\t\t\t// If a play button is clicked we need to *not* hide the player.\n\t\t\t\t// The player visibility is checked to see if the focus player\n\t\t\t\t// should be used.\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( this.isShown() && this.selectionPlayer.isSelectionValid() ) {\n\t\t\t\tthis.showSelectionPlayer();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tthis.isShown() &&\n\t\t\t\tpageContent.contains( event.target ) &&\n\t\t\t\tthis.selectionPlayer.getFocus() &&\n\t\t\t\t!this.player.isPlaying()\n\t\t\t) {\n\t\t\t\t// Show focus player only if we click somewhere we can start\n\t\t\t\t// playing. Don't show the focus player when playing to\n\t\t\t\t// simplify things.\n\t\t\t\tthis.showSelectionPlayer( true );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If there's no reason to show the selection player it means that\n\t\t\t// you can't use it. Therefore hide it.\n\t\t\tthis.hideSelectionPlayer();\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\tisShown() {\n\t\tif ( !this.toolbar ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn this.toolbar.isVisible();\n\t}\n\n\t/**\n\t * Show the selection player close to selection or focus.\n\t *\n\t * When used for selection the player is shown below the end of the\n\t * selection. When used for focus the player is shown above the focus. Both\n\t * are meant to avoid covering relevant text.\n\t *\n\t * @param {boolean} focusPlayer If true puts the player above the relevant\n\t *  position, if false below.\n\t */\n\n\tshowSelectionPlayer( focusPlayer ) {\n\t\tconst selection = window.getSelection();\n\t\tlet left;\n\t\tlet css;\n\t\tconst lastRange = selection.getRangeAt( selection.rangeCount - 1 );\n\t\tconst lastRect = util.getLast( lastRange.getClientRects() );\n\t\t// Showing the element needs to be done before the calculations to get\n\t\t// the dimensions right.\n\t\tthis.selectionPlayerUi.toggle( true );\n\n\t\tif ( focusPlayer ) {\n\t\t\t// Place the player above the clicked text to not obscure any\n\t\t\t// follwing text. Preceding text is probably not as important when\n\t\t\t// you choosing a place to start from.\n\t\t\tconst firstRange = selection.getRangeAt( 0 );\n\t\t\tconst firstRect = firstRange.getClientRects()[ 0 ];\n\t\t\tleft = firstRect.left + $( document ).scrollLeft();\n\t\t\tconst top = firstRect.top + $( document ).scrollTop() -\n\t\t\t\tthis.selectionPlayerUi.$element.height();\n\t\t\tcss = {\n\t\t\t\tleft: left + 'px',\n\t\t\t\ttop: top + 'px'\n\t\t\t};\n\t\t} else {\n\t\t\t// Place the player under the end of the selected text.\n\t\t\tif ( this.getTextDirection( lastRange.endContainer ) === 'rtl' ) {\n\t\t\t\t// For RTL languages, the end of the text is the far left.\n\t\t\t\tleft = lastRect.left + $( document ).scrollLeft();\n\t\t\t} else {\n\t\t\t\t// For LTR languages, the end of the text is the far\n\t\t\t\t// right. This is the default value for the direction\n\t\t\t\t// property.\n\t\t\t\tleft =\n\t\t\t\t\tlastRect.right +\n\t\t\t\t\t$( document ).scrollLeft() -\n\t\t\t\t\tthis.selectionPlayerUi.$element.width();\n\t\t\t}\n\t\t\tconst top = lastRect.bottom + $( document ).scrollTop();\n\t\t\tcss = {\n\t\t\t\tleft: left + 'px',\n\t\t\t\ttop: top + 'px'\n\t\t\t};\n\t\t}\n\n\t\tthis.selectionPlayerUi.$element.css( css );\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\tgetTextDirection( 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 * Hides the selection player.\n\t */\n\n\thideSelectionPlayer() {\n\t\tthis.selectionPlayerUi.toggle( false );\n\t}\n\n\t/**\n\t * Checks if the selection player is shown.\n\t *\n\t * @return {boolean}\n\t */\n\n\tisSelectionPlayerShown() {\n\t\treturn this.selectionPlayerUi.isVisible();\n\t}\n\n\t/**\n\t * Register listeners for keyboard shortcuts.\n\t */\n\n\taddKeyboardShortcuts() {\n\t\tconst shortcuts = mw.config.get( 'wgWikispeechKeyboardShortcuts' );\n\t\t$( document ).on( 'keydown', ( event ) => {\n\t\t\tif ( this.eventMatchShortcut( event, shortcuts.playPause ) ) {\n\t\t\t\tthis.player.playOrPause();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tthis.eventMatchShortcut(\n\t\t\t\t\tevent,\n\t\t\t\t\tshortcuts.stop\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\tthis.player.stop();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tthis.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\tthis.player.skipAheadUtterance();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tthis.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\tthis.player.skipBackUtterance();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tthis.eventMatchShortcut( event, shortcuts.skipAheadWord )\n\t\t\t) {\n\t\t\t\tthis.player.skipAheadToken();\n\t\t\t\treturn false;\n\t\t\t} else if (\n\t\t\t\tthis.eventMatchShortcut( event, shortcuts.skipBackWord )\n\t\t\t) {\n\t\t\t\tthis.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 ( this.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\teventMatchShortcut( 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\taddDialogs() {\n\t\t$( document.body ).append( this.windowManager.$element );\n\t\tthis.messageDialog = new OO.ui.MessageDialog();\n\t\tthis.settingsDialog = new UserOptionsDialog();\n\t\tthis.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\tthis.addWindow( this.messageDialog );\n\t\tthis.addWindow( this.settingsDialog );\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\taddWindow( window ) {\n\t\tthis.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\ttoggleVisibility() {\n\t\tif ( this.isShown() ) {\n\t\t\tthis.toolbar.toggle( false );\n\t\t\tthis.selectionPlayerUi.toggle( false );\n\t\t\tthis.$playerFooter.hide();\n\t\t} else {\n\t\t\tthis.toolbar.toggle( true );\n\t\t\tthis.selectionPlayerUi.toggle( true );\n\t\t\tthis.$playerFooter.show();\n\t\t}\n\t}\n\n\t/**\n\t * Loads the error audio once and calls it in init()\n\t */\n\n\tloadErrorAudio() {\n\t\tconst lang = mw.config.get( 'wgUserLanguage' ) || 'en';\n\t\tlet errorAudioData;\n\n\t\ttry {\n\t\t\terrorAudioData = require( `./audio/error.${ lang }.json` );\n\t\t} catch ( e ) {\n\t\t\terrorAudioData = require( './audio/error.en.json' );\n\t\t}\n\t\tconst src = 'data:audio/ogg;base64,' + errorAudioData[ 'wikispeech-listen' ].audio;\n\n\t\tthis.errorAudio = new Audio( src );\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\tshowLoadAudioError() {\n\t\tif ( this.errorAudio ) {\n\t\t\tthis.errorAudio.play();\n\t\t}\n\n\t\treturn this.openWindow(\n\t\t\tthis.messageDialog,\n\t\t\tthis.errorLoadAudioDialogData\n\t\t).then( ( data ) => {\n\t\t\tif ( this.errorAudio ) {\n\t\t\t\tthis.errorAudio.pause();\n\t\t\t\tthis.errorAudio.currentTime = 0;\n\t\t\t}\n\t\t\treturn data;\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\topenWindow( window, data ) {\n\t\treturn this.windowManager.openWindow( window, data ).closed;\n\t}\n}\n\nmodule.exports = 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":"max-len","severity":1,"message":"This line has a length of 102. Maximum allowed is 100.","line":7,"column":1,"nodeType":"Program","messageId":"max","endLine":7,"endColumn":103},{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":132,"column":3,"nodeType":"CallExpression","endLine":152,"endColumn":6}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"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 and saves preferences to the user's Wikispeech_preferences subpage.\n *\n * On the producer wiki, it behaves like the standard user options page and saves to user preferences.\n *\n * @class ext.wikispeech.UserOptionsDialog\n */\n\nconst util = require( './ext.wikispeech.util.js' );\n\nclass UserOptionsDialog extends OO.ui.ProcessDialog {\n\tconstructor( config ) {\n\t\tsuper( config );\n\n\t\tthis.languageSelect = null;\n\t\tthis.voiceSelect = null;\n\t\tthis.speechRateInput = null;\n\t\tthis.partOfContentInput = null;\n\n\t}\n\n\tinitialize() {\n\t\tsuper.initialize();\n\t\tconst panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );\n\t\tconst content = new OO.ui.FieldsetLayout();\n\t\tconst voiceFieldset = this.addVoiceFieldset();\n\n\t\t// MediaWiki stores user options as strings.\n\t\t// this.speechRateInput expects a string value to match against its options data fields,\n\t\t// therefore we convert the speech rate to a string.\n\t\tconst speechRates = mw.config.get( 'wgWikispeechSpeechRates' );\n\t\tconst speechRate = String( mw.user.options.get( 'wikispeechSpeechRate' ) || 1.0 );\n\n\t\tconst rateOptions = speechRates.map( ( value ) => ( {\n\t\t\tlabel: mw.msg( 'percent', value * 100 ),\n\t\t\tdata: String( value )\n\t\t} ) );\n\n\t\t// Add input field for speech rate, shown in percent.\n\t\tthis.speechRateInput = new OO.ui.DropdownInputWidget( {\n\t\t\toptions: rateOptions,\n\t\t\tvalue: String( speechRate )\n\t\t} );\n\n\t\tconst speechRateFieldset = new OO.ui.FieldsetLayout( {\n\t\t\tlabel: mw.msg( 'prefs-wikispeech-speech-rate-percent' )\n\t\t} );\n\t\tconst speechRateField = new OO.ui.FieldLayout( this.speechRateInput );\n\t\tspeechRateFieldset.addItems( [ speechRateField ] );\n\n\t\t// MediaWiki stores booleans as strings: '1' for true, '' for false.\n\t\tconst partOfContentInputRaw = mw.user.options.get( 'wikispeechPartOfContent' );\n\t\tconst partOfContentInput = partOfContentInputRaw === '1';\n\n\t\t// Adding extra part of content\n\t\tthis.partOfContent = new OO.ui.CheckboxInputWidget( {\n\t\t\tselected: partOfContentInput\n\t\t} );\n\t\tconst layout = new OO.ui.FieldLayout(\n\t\t\tthis.partOfContent,\n\t\t\t{\n\t\t\t\tlabel: mw.msg( 'prefs-wikispeech-part-of-content' ),\n\t\t\t\talign: 'inline'\n\t\t\t}\n\t\t);\n\n\t\t// Add a notice about needing to reload the page before\n\t\t// preferences kick in.\n\t\tconst notice = new OO.ui.MessageWidget( {\n\t\t\ttype: 'notice',\n\t\t\tlabel: mw.msg( 'wikispeech-notice-prefs-apply-on-next-page-load' )\n\t\t} );\n\t\tconst noticeFieldset = new OO.ui.FieldsetLayout();\n\t\tnoticeFieldset.addItems( [ new OO.ui.FieldLayout( notice, {\n\t\t\t// HACK: This is not a nice MW way of organize the layout,\n\t\t\t// Instead, find another OO.ui/Codex element that has the margins predefined.\n\t\t\tclasses: [ 'wikispeech-margin-top' ]\n\t\t} ) ] );\n\n\t\tcontent.addItems( [\n\t\t\tvoiceFieldset,\n\t\t\tspeechRateFieldset,\n\t\t\tlayout,\n\t\t\tnoticeFieldset\n\t\t] );\n\n\t\t// Add link to preferences page if not in gadget\n\t\tif ( !mw.config.get( 'wgWikispeechProducerUrl' ) ) {\n\t\t\tconst prefsLink = new OO.ui.LabelWidget( {\n\t\t\t\tlabel: $( '<a>' )\n\t\t\t\t\t.attr( 'href', mw.util.getUrl( 'Special:Preferences#mw-prefsection-wikispeech' ) )\n\t\t\t\t\t.css( { display: 'block', marginTop: '1em' } )\n\t\t\t\t\t.text( mw.msg( 'prefs-wikispeech-view-all' ) )\n\t\t\t} );\n\t\t\tcontent.addItems( [ prefsLink ] );\n\t\t}\n\n\t\tpanel.$element.append( content.$element );\n\t\tthis.$body.append( panel.$element );\n\t}\n\n\t/**\n\t * Add fields for selecting voice.\n\t *\n\t * Adds two fields: language and voice. When a language is\n\t * selected, the voice is populated by the available voices for\n\t * that language. Language defaults to the language of the current\n\t * page. Voices are labeled with language code and autonym.\n\t *\n\t * @return {OO.ui.FieldsetLayout}\n\t */\n\n\taddVoiceFieldset() {\n\t\tconst voices = mw.config.get( 'wgWikispeechVoices' );\n\t\tconst languageItems = [];\n\t\tconst languageCodes = Object.keys( voices );\n\t\tlanguageCodes.sort();\n\t\tlanguageCodes.forEach( ( language ) => {\n\t\t\tlanguageItems.push(\n\t\t\t\tnew OO.ui.MenuOptionWidget( {\n\t\t\t\t\tdata: language,\n\t\t\t\t\tlabel: language\n\t\t\t\t} )\n\t\t\t);\n\t\t} );\n\t\t// Add autonyms to labels. Do this separately to not break if\n\t\t// the request fails. If it does, we still have the language\n\t\t// codes as labels.\n\t\tnew mw.Api().get( {\n\t\t\taction: 'query',\n\t\t\tformat: 'json',\n\t\t\tformatversion: 2,\n\t\t\tmeta: 'languageinfo',\n\t\t\tliprop: 'autonym',\n\t\t\tlicode: languageCodes\n\t\t} ).done( ( response ) => {\n\t\t\tconst info = response.query.languageinfo;\n\t\t\tObject.keys( info ).forEach( ( code ) => {\n\t\t\t\tlanguageItems.forEach( ( item ) => {\n\t\t\t\t\tif ( item.label === code ) {\n\t\t\t\t\t\titem.setLabel( code + ' - ' + info[ code ].autonym );\n\t\t\t\t\t}\n\t\t\t\t} );\n\t\t\t} );\n\t\t\t// Reselect the language to show the new label.\n\t\t\tthis.languageSelect.getMenu().selectItemByData(\n\t\t\t\tmw.config.get( 'wgPageContentLanguage' )\n\t\t\t);\n\t\t} );\n\t\tthis.languageSelect = new OO.ui.DropdownWidget( {\n\t\t\tmenu: {\n\t\t\t\titems: languageItems\n\t\t\t}\n\t\t} );\n\n\t\tthis.voiceSelect = new OO.ui.DropdownWidget();\n\t\t// Update the voice items when language is selected.\n\t\tthis.languageSelect.getMenu().on( 'select', ( item ) => {\n\t\t\tconst voiceItems = [\n\t\t\t\tnew OO.ui.MenuOptionWidget( {\n\t\t\t\t\tdata: '',\n\t\t\t\t\tlabel: mw.msg( 'default' )\n\t\t\t\t} )\n\t\t\t];\n\t\t\tconst language = item.data;\n\t\t\tvoices[ language ].forEach( ( voice ) => {\n\t\t\t\tvoiceItems.push(\n\t\t\t\t\tnew OO.ui.MenuOptionWidget( {\n\t\t\t\t\t\tdata: voice,\n\t\t\t\t\t\tlabel: voice\n\t\t\t\t\t} )\n\t\t\t\t);\n\t\t\t} );\n\t\t\tthis.voiceSelect.getMenu().clearItems();\n\t\t\tthis.voiceSelect.getMenu().addItems( voiceItems );\n\t\t\tconst currentVoice = util.getUserVoice( language );\n\t\t\tthis.voiceSelect.getMenu().selectItemByData( currentVoice );\n\t\t} );\n\t\t// Select the language for the current page, since that is\n\t\t// probably the one the user is interested in.\n\t\tthis.languageSelect.getMenu().selectItemByData(\n\t\t\tmw.config.get( 'wgPageContentLanguage' )\n\t\t);\n\n\t\tconst fieldset = new OO.ui.FieldsetLayout(\n\t\t\t{ label: mw.msg( 'prefs-wikispeech-voice' ) }\n\t\t);\n\t\tconst languageField = new OO.ui.FieldLayout(\n\t\t\tthis.languageSelect,\n\t\t\t{ label: mw.msg( 'wikispeech-language' ) }\n\t\t);\n\t\tconst voiceField = new OO.ui.FieldLayout(\n\t\t\tthis.voiceSelect,\n\t\t\t{ label: mw.msg( 'prefs-wikispeech-voice' ) }\n\t\t);\n\t\tfieldset.addItems( [ languageField, voiceField ] );\n\t\treturn fieldset;\n\t}\n\n\t/**\n\t * Handle actions.\n\t *\n\t * Closes the dialog when \"Save\" is clicked.\n\t *\n\t * @param {Object} action\n\t * @return {OO.ui.Process}\n\t */\n\n\tgetActionProcess( action ) {\n\t\tif ( action ) {\n\t\t\treturn new OO.ui.Process( () => {\n\t\t\t\tthis.close( { action: action } );\n\t\t\t} );\n\t\t}\n\t\treturn UserOptionsDialog.super.prototype.getActionProcess.call( this, action );\n\t}\n\n\t/**\n\t * Get the selected language and voice.\n\t *\n\t * @return {Object}\n\t * @return {string} return.variable User option variable name.\n\t * @return {string} return.voice Name of voice.\n\t */\n\n\tgetVoice() {\n\t\tconst language = this.languageSelect.getMenu().findSelectedItem().data;\n\t\tconst voiceVariable = util.getVoiceConfigVariable( language );\n\t\tconst voice = this.voiceSelect.getMenu().findSelectedItem().data;\n\t\treturn { variable: voiceVariable, voice: voice };\n\t}\n\n\t/**\n\t * Get the selected speech rate.\n\t *\n\t * @return {number} Speech rate as a decimal number, i.e. 100% =\n\t *  1.0.\n\t */\n\n\tgetSpeechRate() {\n\t\treturn parseFloat( this.speechRateInput.value );\n\t}\n\n\t/**\n\t * Get the check for reading parts of content\n\t *\n\t * @return {boolean} True if the checkbox is checked, otherwise false.\n\t */\n\tgetPartOfContent() {\n\t\treturn this.partOfContent.isSelected();\n\t}\n}\n\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\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/abstractSchemaChanges/patch-wikispeech_utterance-wsu_message_key.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.feedback.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.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":56,"nodeType":"CallExpression","endLine":40,"endColumn":76,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-parse-html-literal","severity":2,"message":"Prefer DOM building to parsing HTML literals","line":88,"column":56,"nodeType":"CallExpression","endLine":88,"endColumn":76,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-parse-html-literal","severity":2,"message":"Prefer DOM building to parsing HTML literals","line":118,"column":56,"nodeType":"CallExpression","endLine":118,"endColumn":76,"suppressions":[{"kind":"directive","justification":""}]},{"ruleId":"no-jquery/no-parse-html-literal","severity":2,"message":"Prefer DOM building to parsing HTML literals","line":148,"column":56,"nodeType":"CallExpression","endLine":148,"endColumn":79,"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":[{"ruleId":"no-jquery/no-done-fail","severity":1,"message":"Prefer .then to .done","line":67,"column":2,"nodeType":"CallExpression","endLine":80,"endColumn":5}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"const Ui = require( 'ext.wikispeech/ext.wikispeech.ui.js' );\nconst Player = require( 'ext.wikispeech/ext.wikispeech.player.js' );\nconst SelectionPlayer = require( 'ext.wikispeech/ext.wikispeech.selectionPlayer.js' );\nconst util = require( './ext.wikispeech.test.util.js' );\n\nQUnit.module( 'ext.wikispeech.ui', QUnit.newMwEnvironment( {\n\tbeforeEach: function () {\n\t\tthis.ui = new Ui();\n\t\tthis.ui.player = sinon.createStubInstance( Player );\n\t\tthis.ui.selectionPlayer = sinon.stub( new SelectionPlayer() );\n\t\t$( '#qunit-fixture' ).append(\n\t\t\t$( '<div>' ).attr( 'id', 'content' ),\n\t\t\t$( '<div>' ).attr( 'id', 'footer' )\n\t\t);\n\t\tsinon.stub( this.ui, 'isShown' ).returns( true );\n\t\tthis.contentSelector = '#mw-content-text';\n\t\tmw.config.set( 'wgWikispeechContentSelector', this.contentSelector );\n\t\tthis.$content = util.setContentHtml( 'Some text.' );\n\n\t\t/**\n\t\t * Stub window.getSelection\n\t\t *\n\t\t * @param {Node} startContainer Node where selection starts.\n\t\t * @param {Node} endContainer Node where selection ends.\n\t\t * @param {DOMRect} rect The selection rectangle.\n\t\t */\n\t\tthis.stubGetSelection = ( startContainer, endContainer, rect ) => {\n\t\t\tthis.sandbox.stub( window, 'getSelection' ).returns( {\n\t\t\t\trangeCount: 1,\n\t\t\t\tgetRangeAt: function () {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tgetClientRects: function () {\n\t\t\t\t\t\t\treturn [ rect ];\n\t\t\t\t\t\t},\n\t\t\t\t\t\tstartContainer: startContainer,\n\t\t\t\t\t\tendContainer: endContainer\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t} );\n\t\t};\n\t},\n\tafterEach: function () {\n\t\t// Remove the event listeners to not trigger them after\n\t\t// the tests have run.\n\t\t$( document ).off( 'mouseup' );\n\t\tthis.$content.off( 'click' );\n\t\t$( '#qunit-fixture' ).empty();\n\t\t$( '.ext-wikispeech-control-panel, .ext-wikispeech-selection-player' ).remove();\n\t}\n} ) );\n\nQUnit.test( 'addControlPanel(): adds help menu item if page is set', function ( assert ) {\n\tconst done = assert.async();\n\n\tmw.config.set( 'wgArticlePath', '/wiki/$1' );\n\tmw.config.set( 'wgWikispeechHelpPage', 'Help' );\n\n\tsinon.stub( this.ui, 'addBufferingIcon' );\n\tconst addMenuItemStub = sinon.stub( this.ui, 'addMenuItem' );\n\n\tconst deferred = $.Deferred();\n\tsinon.stub( mw.Api.prototype, 'getUserInfo' ).returns( deferred.promise() );\n\n\tthis.ui.addControlPanel();\n\tdeferred.resolve( { rights: [] } );\n\n\tdeferred.done( () => {\n\t\tassert.strictEqual( addMenuItemStub.called, true, 'addMenuItem called' );\n\n\t\tconst helpItem = addMenuItemStub\n\t\t\t.getCalls()\n\t\t\t.map( ( i ) => i.args[ 0 ] )\n\t\t\t.find( ( item ) => item.icon === 'help' );\n\n\t\tassert.notStrictEqual( helpItem, undefined, 'Help menu item exists' );\n\n\t\tmw.Api.prototype.getUserInfo.restore();\n\t\tthis.ui.addMenuItem.restore();\n\t\tdone();\n\t} );\n} );\n\nQUnit.test( 'addControlPanel(): adds feedback menu item if page is set', function ( assert ) {\n\tconst done = assert.async();\n\tmw.config.set( 'wgArticlePath', '/wiki/$1' );\n\tmw.config.set( 'wgWikispeechFeedbackPage', 'Feedback' );\n\tsinon.stub( this.ui, 'addBufferingIcon' );\n\n\tconst addMenuItemStub = sinon.stub( this.ui, 'addMenuItem' );\n\tconst deferred = $.Deferred();\n\tsinon.stub( mw.Api.prototype, 'getUserInfo' ).returns( deferred.promise() );\n\n\tthis.ui.addControlPanel();\n\tdeferred.resolve( { rights: [] } );\n\n\tdeferred.then( () => {\n\t\tassert.strictEqual( addMenuItemStub.called, true, 'addMenuItem called' );\n\n\t\tconst feedbackItem = addMenuItemStub\n\t\t\t.getCalls()\n\t\t\t.map( ( i ) => i.args[ 0 ] )\n\t\t\t.find( ( item ) => item.icon === 'feedback' );\n\n\t\tassert.notStrictEqual( feedbackItem, undefined, 'Feedback menu item exists' );\n\n\t\tmw.Api.prototype.getUserInfo.restore();\n\t\tthis.ui.addMenuItem.restore();\n\t\tdone();\n\t} );\n} );\n\nQUnit.test( 'createEditButton(): returns menu item with local URL', function ( assert ) {\n\tmw.config.set( 'wgPageContentLanguage', 'en' );\n\tmw.config.set( 'wgArticleId', 1 );\n\tmw.config.set( 'wgScript', '/wiki/index.php' );\n\n\tconst item = this.ui.createEditButton();\n\n\tassert.strictEqual(\n\t\titem.url,\n\t\t'/wiki/index.php?title=Special%3AEditLexicon&language=en&page=1',\n\t\t{\n\t\t\ttitle: mw.msg( 'wikispeech-edit-lexicon-btn' ),\n\t\t\ticon: 'edit',\n\t\t\tid: 'wikispeech-edit'\n\t\t}\n\t);\n\n} );\n\nQUnit.test( 'createEditButton(): add edit button with link to given script URL', function ( assert ) {\n\tmw.config.set( 'wgWikispeechAllowConsumerEdits', true );\n\tmw.config.set( 'wgPageContentLanguage', 'en' );\n\tmw.config.set( 'wgArticleId', 1 );\n\n\tconst item = this.ui.createEditButton( 'http://producer.url/w/index.php' );\n\n\tassert.strictEqual(\n\t\titem.url,\n\t\t// The colon in \"Special:EditLexicon\" is URL encoded, see:\n\t\t// https://url.spec.whatwg.org/#concept-urlencoded-serializer.\n\t\t'http://producer.url/w/index.php?title=Special%3AEditLexicon&language=en&page=1',\n\t\t{\n\t\t\ttitle: mw.msg( 'wikispeech-edit-lexicon-btn' ),\n\t\t\ticon: 'edit',\n\t\t\tid: 'wikispeech-edit'\n\t\t}\n\t);\n} );\n\nQUnit.test( 'createEditButton(): add edit button with link to given script URL, with consumerUrl parameter', function ( assert ) {\n\tmw.config.set( 'wgWikispeechAllowConsumerEdits', true );\n\tmw.config.set( 'wgPageContentLanguage', 'en' );\n\tmw.config.set( 'wgArticleId', 1 );\n\tmw.config.set( 'wgScriptPath', '/' );\n\n\tconst scriptUrl = 'http://producer.url/w/index.php';\n\tconst consumerUrl = window.location.origin + '/';\n\n\tconst item = this.ui.createEditButton( scriptUrl, consumerUrl );\n\tconst expectedUrl = scriptUrl + '?' + new URLSearchParams( {\n\t\ttitle: 'Special:EditLexicon',\n\t\tlanguage: 'en',\n\t\tpage: 1,\n\t\tconsumerUrl: consumerUrl\n\t} ).toString();\n\n\tassert.strictEqual(\n\t\titem.url,\n\t\texpectedUrl,\n\t\t{\n\t\t\ttitle: mw.msg( 'wikispeech-edit-lexicon-btn' ),\n\t\t\ticon: 'edit',\n\t\t\tid: 'wikispeech-edit'\n\t\t}\n\t);\n} );\n\nQUnit.test( 'showBufferingIconIfAudioIsLoading()', function () {\n\tthis.ui.$bufferingIcons = sinon.stub( $( '<div>' ) );\n\tconst mockUtterance = { audio: { readyState: 0 } };\n\n\tthis.ui.showBufferingIconIfAudioIsLoading( mockUtterance );\n\n\tsinon.assert.called( this.ui.$bufferingIcons.show );\n} );\n\nQUnit.test( 'showBufferingIconIfAudioIsLoading(): already loaded', function () {\n\tthis.ui.$bufferingIcons = sinon.stub( $( '<div>' ) );\n\tconst mockUtterance = { audio: { readyState: 2 } };\n\n\tthis.ui.showBufferingIconIfAudioIsLoading( mockUtterance );\n\n\tsinon.assert.notCalled( this.ui.$bufferingIcons.show );\n} );\n\nQUnit.test( 'addSelectionPlayer(): mouse up shows selection player', function () {\n\tutil.setContentHtml( 'LTR text.' );\n\tconst textNode = $( this.contentSelector ).contents().get( 0 );\n\tthis.ui.selectionPlayer.isSelectionValid.returns( true );\n\tthis.ui.playPauseButton = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.stubGetSelection( textNode, textNode, { right: 50, bottom: 10 } );\n\tthis.ui.addSelectionPlayer();\n\tthis.ui.selectionPlayerUi.$element.width( 30 );\n\tsinon.spy( this.ui.selectionPlayerUi.$element, 'css' );\n\tsinon.spy( this.ui.selectionPlayerUi, 'toggle' );\n\tconst event = $.Event( 'mouseup' );\n\n\t$( document ).triggerHandler( event );\n\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, true );\n\tsinon.assert.calledWith(\n\t\tthis.ui.selectionPlayerUi.$element.css,\n\t\t{\n\t\t\tleft: '20px',\n\t\t\ttop: 10 + $( document ).scrollTop() + 'px'\n\t\t}\n\t);\n} );\n\nQUnit.test( 'addSelectionPlayer(): mouse up shows focus player', function () {\n\tconst $content = util.setContentHtml( 'LTR text.' );\n\tconst textNode = $( this.contentSelector ).contents().get( 0 );\n\tthis.stubGetSelection( textNode, textNode, { left: 20, top: 30 } );\n\tthis.ui.selectionPlayer.isSelectionValid.returns( false );\n\tthis.ui.selectionPlayer.getFocus = sinon.stub().returns( true );\n\tthis.ui.playPauseButton = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.ui.addSelectionPlayer();\n\tthis.ui.selectionPlayerUi.$element.height( 20 );\n\tsinon.spy( this.ui.selectionPlayerUi.$element, 'css' );\n\tsinon.spy( this.ui.selectionPlayerUi, 'toggle' );\n\tconst event = $.Event( 'mouseup' );\n\tevent.target = $content.get( 0 );\n\n\t$( document ).triggerHandler( event );\n\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, true );\n\tsinon.assert.calledWith(\n\t\tthis.ui.selectionPlayerUi.$element.css,\n\t\t{\n\t\t\tleft: '20px',\n\t\t\ttop: 10 + $( document ).scrollTop() + 'px'\n\t\t}\n\t);\n} );\n\nQUnit.test( 'addSelectionPlayer(): mouse up shows selection player, RTL', function () {\n\tutil.setContentHtml(\n\t\t'<b style=\"direction: rtl\">RTL text.</b>'\n\t);\n\tconst textNode = $( this.contentSelector + ' b' ).contents().get( 0 );\n\tthis.ui.selectionPlayer.isSelectionValid.returns( true );\n\tthis.ui.playPauseButton = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.stubGetSelection( textNode, textNode, { left: 15, bottom: 10 } );\n\tthis.ui.addSelectionPlayer();\n\tsinon.spy( this.ui.selectionPlayerUi.$element, 'css' );\n\tsinon.spy( this.ui.selectionPlayerUi, 'toggle' );\n\tconst event = $.Event( 'mouseup' );\n\n\t$( document ).triggerHandler( event );\n\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, true );\n\tsinon.assert.calledWith(\n\t\tthis.ui.selectionPlayerUi.$element.css,\n\t\t{\n\t\t\tleft: '15px',\n\t\t\ttop: 10 + $( document ).scrollTop() + 'px'\n\t\t}\n\t);\n} );\n\nQUnit.test( 'addSelectionPlayer(): mouse up hides selection player when start of selection is not in an utterance node', function () {\n\tutil.setContentHtml(\n\t\t'<del>Not an utterance.</del> An utterance.'\n\t);\n\tconst notUtteranceNode = $( this.contentSelector + ' del' ).contents().get( 0 );\n\tconst utteranceNode = $( this.contentSelector ).contents().get( 1 );\n\tthis.ui.playPauseButton = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.ui.addSelectionPlayer();\n\tsinon.spy( this.ui.selectionPlayerUi, 'toggle' );\n\tthis.stubGetSelection( notUtteranceNode, utteranceNode );\n\tconst event = new MouseEvent( 'mouseup' );\n\n\tdocument.dispatchEvent( event );\n\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, false );\n} );\n\nQUnit.test( 'addSelectionPlayer(): mouse up hides selection player when end of selection is not in an utterance node', function () {\n\tutil.setContentHtml(\n\t\t'An utterance. <del>Not an utterance.</del>'\n\t);\n\tconst notUtteranceNode = $( this.contentSelector + ' del' ).contents().get( 0 );\n\tconst utteranceNode = $( this.contentSelector ).contents().get( 0 );\n\tthis.ui.playPauseButton = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.ui.addSelectionPlayer();\n\tsinon.spy( this.ui.selectionPlayerUi, 'toggle' );\n\tthis.stubGetSelection( utteranceNode, notUtteranceNode );\n\tconst event = new MouseEvent( 'mouseup' );\n\n\tdocument.dispatchEvent( event );\n\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, false );\n} );\n\nQUnit.test( 'addSelectionPlayer(): do not show if UI is hidden', function () {\n\tutil.setContentHtml( 'LTR text.' );\n\tconst textNode = $( this.contentSelector ).contents().get( 0 );\n\tthis.ui.isShown.returns( false );\n\tthis.ui.playPauseButton = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.ui.addSelectionPlayer();\n\tthis.ui.selectionPlayer.isSelectionValid.returns( true );\n\tsinon.spy( this.ui.selectionPlayerUi, 'toggle' );\n\tthis.stubGetSelection( textNode, textNode );\n\tconst event = $.Event( 'mouseup' );\n\n\t$( document ).triggerHandler( event );\n\n\tsinon.assert.neverCalledWith( this.ui.selectionPlayerUi.toggle, true );\n} );\n\nQUnit.test( 'addSelectionPlayer(): hide selection player initially', function ( assert ) {\n\tthis.ui.addSelectionPlayer();\n\n\tassert.false( this.ui.selectionPlayerUi.isVisible() );\n} );\n\nQUnit.test( 'showLoadAudioError(): plays and stops the error audio', function ( assert ) {\n\tconst done = assert.async();\n\tconst audioMock = {\n\t\tplay: sinon.stub(),\n\t\tpause: sinon.stub(),\n\t\tcurrentTime: 123\n\t};\n\tthis.ui.errorAudio = audioMock;\n\tsinon.stub( this.ui, 'openWindow' ).resolves( { action: 'stop' } );\n\tthis.ui.showLoadAudioError().then( () => {\n\t\tassert.strictEqual( audioMock.play.calledOnce, true );\n\t\tassert.strictEqual( audioMock.pause.calledOnce, true );\n\t\tassert.strictEqual( audioMock.currentTime, 0 );\n\t\tdone();\n\t} );\n} );\n\n/**\n * Create a keydown event.\n *\n * @param {number} keyCode The key code for the event.\n * @param {string} modifiers A string that defines the\n *  modifiers. The characters c, a and s triggers the modifiers\n *  for ctrl, alt and shift, respectively.\n * @return {jQuery} The created keydown event.\n */\nfunction createKeydownEvent( keyCode, modifiers ) {\n\tconst event = $.Event( 'keydown' );\n\tevent.which = keyCode;\n\tevent.ctrlKey = modifiers.includes( 'c' );\n\tevent.altKey = modifiers.includes( 'a' );\n\tevent.shiftKey = modifiers.includes( 's' );\n\treturn event;\n}\n\n/**\n * Test that a keyboard event triggers the correct function.\n *\n * @param {QUnit.assert} assert\n * @param {string} functionName Name of the function that should\n *  be called.\n * @param {number} keyCode The key code for the event.\n * @param {string} modifiers A string that defines the\n *  modifiers. The characters c, a and s triggers the modifiers\n *  for ctrl, alt and shift, respectively.\n */\nfunction testKeyboardShortcut( assert, functionName, keyCode, modifiers ) {\n\tmw.config.set(\n\t\t'wgWikispeechKeyboardShortcuts', {\n\t\t\tplayPause: {\n\t\t\t\tkey: 13,\n\t\t\t\tmodifiers: [ 'ctrl' ]\n\t\t\t},\n\t\t\tstop: {\n\t\t\t\tkey: 8,\n\t\t\t\tmodifiers: [ 'ctrl' ]\n\t\t\t},\n\t\t\tskipAheadSentence: {\n\t\t\t\tkey: 39,\n\t\t\t\tmodifiers: [ 'ctrl' ]\n\t\t\t},\n\t\t\tskipBackSentence: {\n\t\t\t\tkey: 37,\n\t\t\t\tmodifiers: [ 'ctrl' ]\n\t\t\t},\n\t\t\tskipAheadWord: {\n\t\t\t\tkey: 40,\n\t\t\t\tmodifiers: [ 'ctrl' ]\n\t\t\t},\n\t\t\tskipBackWord: {\n\t\t\t\tkey: 38,\n\t\t\t\tmodifiers: [ 'ctrl' ]\n\t\t\t}\n\t\t}\n\t);\n\tthis.ui.addKeyboardShortcuts();\n\n\t$( document ).triggerHandler( createKeydownEvent( keyCode, modifiers ) );\n\n\tassert.strictEqual( this.ui.player[ functionName ].called, true );\n}\n\nQUnit.test( 'Pressing keyboard shortcut for play/pause', function ( assert ) {\n\ttestKeyboardShortcut.call( this, assert, 'playOrPause', 13, 'c' );\n} );\n\nQUnit.test( 'Pressing keyboard shortcut for stop', function ( assert ) {\n\ttestKeyboardShortcut.call( this, assert, 'stop', 8, 'c' );\n} );\n\nQUnit.test( 'Pressing keyboard shortcut for skipping ahead sentence', function ( assert ) {\n\ttestKeyboardShortcut.call( this, assert, 'skipAheadUtterance', 39, 'c' );\n} );\n\nQUnit.test( 'Pressing keyboard shortcut for skipping back sentence', function ( assert ) {\n\ttestKeyboardShortcut.call( this, assert, 'skipBackUtterance', 37, 'c' );\n} );\n\nQUnit.test( 'Pressing keyboard shortcut for skipping ahead word', function ( assert ) {\n\ttestKeyboardShortcut.call( this, assert, 'skipAheadToken', 40, 'c' );\n} );\n\nQUnit.test( 'Pressing keyboard shortcut for skipping back word', function ( assert ) {\n\ttestKeyboardShortcut.call( this, assert, 'skipBackToken', 38, 'c' );\n} );\n\nQUnit.test( 'toggleVisibility(): hide', function () {\n\tthis.ui.toolbar = sinon.stub( new OO.ui.Toolbar() );\n\tthis.ui.selectionPlayerUi = sinon.stub( new OO.ui.ButtonGroupWidget() );\n\tthis.ui.$playerFooter = sinon.stub( $( '<div>' ) );\n\n\tthis.ui.toggleVisibility();\n\n\tsinon.assert.calledWith( this.ui.toolbar.toggle, false );\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, false );\n\tsinon.assert.called( this.ui.$playerFooter.hide );\n} );\n\nQUnit.test( 'toggleVisibility(): show', function () {\n\tthis.ui.toolbar = sinon.stub( new OO.ui.Toolbar() );\n\tthis.ui.selectionPlayerUi = sinon.stub( new OO.ui.ButtonWidget() );\n\tthis.ui.$playerFooter = sinon.stub( $( '<div>' ) );\n\tthis.ui.isShown.returns( false );\n\n\tthis.ui.toggleVisibility();\n\n\tsinon.assert.calledWith( this.ui.toolbar.toggle, true );\n\tsinon.assert.calledWith( this.ui.selectionPlayerUi.toggle, true );\n\tsinon.assert.called( this.ui.$playerFooter.show );\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/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 ---
$ /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 466 packages, and audited 467 packages in 6s

109 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

--- 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
  19:1  warning  This line has a length of 118. Maximum allowed is 100  max-len
  24:1  warning  This line has a length of 117. Maximum allowed is 100  max-len
  30:1  warning  This line has a length of 118. Maximum allowed is 100  max-len
  49:1  warning  This line has a length of 119. Maximum allowed is 100  max-len
  56:1  warning  This line has a length of 117. Maximum allowed is 100  max-len
  70:1  warning  This line has a length of 107. Maximum allowed is 100  max-len
  79:1  warning  This line has a length of 127. Maximum allowed is 100  max-len

/src/repo/modules/ext.wikispeech.feedback.js
  145:1  warning  Missing JSDoc @param "params.storage" type             jsdoc/require-param-type
  146:1  warning  Missing JSDoc @param "params.selectionPlayer" type     jsdoc/require-param-type
  220:1  warning  This line has a length of 102. Maximum allowed is 100  max-len
  228:3  warning  Prefer .then to .done                                  no-jquery/no-done-fail
  229:4  warning  Prefer .then to .done                                  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.gadget.js
  33:1  warning  Missing JSDoc @param "mainInstance" type  jsdoc/require-param-type
  39:3  warning  Prefer .then to .done                     no-jquery/no-done-fail

/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
  129:1  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.player.js
  246:3  warning  Prefer .then to .done  no-jquery/no-done-fail
  246:3  warning  Prefer .then to .fail  no-jquery/no-done-fail
  259:5  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.selectionPlayer.js
  124:3  warning  Prefer .then to .done  no-jquery/no-done-fail
  158:3  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.sharedUserOptionSettings.js
   19:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
   31:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
   31:3  warning  Prefer .then to .fail                                     no-jquery/no-done-fail
   69:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
   70:1  warning  Missing JSDoc @param "isProducer" type                    jsdoc/require-param-type
   89:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
  115:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
  116:1  warning  The type 'ext.wikispeech.UserOptionsDialog' is undefined  jsdoc/no-undefined-types
  117:1  warning  Missing JSDoc @param "isProducer" type                    jsdoc/require-param-type
  133:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
  133:3  warning  Prefer .then to .fail                                     no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.storage.js
  259:19  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.transcriptionPreviewer.js
  37:1   warning  This line has a length of 103. Maximum allowed is 100  max-len
  66:19  warning  Prefer .then to .done                                  no-jquery/no-done-fail
  66:19  warning  Prefer .then to .fail                                  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.ui.js
   68:18  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  199:3   warning  Prefer .then to .done                                                                                no-jquery/no-done-fail
  249:1   warning  This line has a length of 107. Maximum allowed is 100                                                max-len
  331:1   warning  This line has a length of 116. Maximum allowed is 100                                                max-len
  428:30  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  436:23  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  742:21  warning  Found non-literal argument in require                                                                security/detect-non-literal-require

/src/repo/modules/ext.wikispeech.userOptionsDialog.js
    7:1  warning  This line has a length of 102. Maximum allowed is 100  max-len
  132:3  warning  Prefer .then to .done                                  no-jquery/no-done-fail

/src/repo/tests/qunit/ext.wikispeech.ui.test.js
  67:2  warning  Prefer .then to .done  no-jquery/no-done-fail

✖ 46 problems (0 errors, 46 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 ---
Upgrading c:mediawiki/mediawiki-codesniffer from 50.0.0 -> 51.0.0
$ /usr/bin/composer update
--- stderr ---
Loading composer repositories with package information
Updating dependencies
Lock file operations: 0 installs, 3 updates, 0 removals
  - Upgrading composer/spdx-licenses (1.5.10 => 1.6.0)
  - Upgrading mediawiki/mediawiki-codesniffer (v50.0.0 => v51.0.0)
  - Upgrading phpcsstandards/phpcsextra (1.4.0 => 1.5.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 0 installs, 3 updates, 0 removals
    0 [>---------------------------]    0 [->--------------------------]
  - Upgrading phpcsstandards/phpcsextra (1.4.0 => 1.5.0): Extracting archive
  - Upgrading composer/spdx-licenses (1.5.10 => 1.6.0): Extracting archive
  - Upgrading mediawiki/mediawiki-codesniffer (v50.0.0 => v51.0.0): Extracting archive
 0/3 [>---------------------------]   0%
 3/3 [============================] 100%
Generating autoload files
16 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
No security vulnerability advisories found.
--- stdout ---

--- end ---
$ vendor/bin/phpcs --report=json
--- stdout ---
{"totals":{"errors":0,"warnings":0,"fixable":0},"files":{"\/src\/repo\/dev\/10-wikispeech.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/RemoteWikiPageProviderException.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentResponse.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/InputTextValidator.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentContent.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PartOfContent\/PartOfContent.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/DefaultUserOptions.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/Segmenter.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/DeletedRevisionException.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Sv\/SwedishFilter.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/CleanedText.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PartOfContent\/TableCell.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PartOfContent\/Link.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentPageResponse.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PageRevisionProperties.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/DigitsToWords.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentList.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/AbstractDigitsToWords.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/OutdatedOrInvalidRevisionException.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentMessageResponse.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Hooks\/DatabaseHooks.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/SpeechoidConnectorException.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Filter.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/FilterRule.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PartOfContent\/Table.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/AbstractPageProvider.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/maintenance\/preSynthesizeMessages.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/Wikispeech.alias.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Namespaces.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesByExpirationDateOnFileJobQueue.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/LocalWikiPageProvider.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PartOfContent\/TableHeader.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/RegexFilterRule.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Hooks\/LexiconArticleEditHooks.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Sv\/DateRule.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/NullEditLexiconException.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesFromStoreByLanguageAndVoiceJobQueue.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesFromStoreByExpirationJob.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/RemoteWikiPageProvider.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconLocalStorage.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Sv\/YearRangeRule.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentMessagesFactory.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/maintenance\/populateSpeechoidLexiconFromWiki.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/maintenance\/flushUtterances.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/VoiceHandler.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/FilterPart.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesFromStoreByLanguageAndVoiceJob.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentFactory.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Hooks\/ApiHooks.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/maintenance\/flushUtterancesByExpirationDateOnFile.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/PageProvider.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesFromStoreByPageIdJob.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Api\/ApiWikispeechSegment.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Api\/ListenMetricsEntryJournal.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/Segment.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/Sv\/SwedishFilterTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconEntryMapper.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/PageFileGenerator.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/maintenance\/generatePageFile.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/ConfiguredLexiconStorage.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/RegexFilter.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/PopulateSpeechoidLexiconFromWikiTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Special\/LanguageOptionsTraitTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Api\/ListenMetricsEntryFileJournal.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/Sv\/YearRuleTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/HaproxyStatusParser.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Specials\/SpecialTestListen.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Api\/ListenMetricsEntrySerializer.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/HaproxyStatusParserTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/RegisteredJobsTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Specials\/LanguageOptionsTrait.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesFromStoreByExpirationJobQueue.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/RemoteWikiPageProviderTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/SpeechoidConnectorTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/ServiceWiring.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/ConfigurationValidator.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/StandardSegmenter.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/Cleaner.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Utterance\/UtteranceStoreRemoteWikiHashTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesByExpirationDateOnFileJob.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Sv\/DigitsToSwedishWords.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/Sv\/DateRuleTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/WikispeechServices.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Sv\/NumberRule.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Utterance\/UtteranceTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/RegexFilterTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/Sv\/DigitsToSwedishWordsTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Utterance\/UtteranceStoreUrlPathFactoryTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/RegexFilterRuleTestBase.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Api\/ListenMetricsEntry.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/ConfigurationValidatorTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconHandler.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/Sv\/YearRangeRuleTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconWanCacheStorage.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/UtteranceGenerator.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconEntry.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/TextFilter\/Sv\/YearRule.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconWikiStorage.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Hooks\/LexiconArticleEditHooksTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/SegmentListTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/FlushUtterancesFromStoreByPageIdJobQueue.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/TextFilter\/Sv\/NumberRuleTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/ApiWikispeechSegmentTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Lexicon\/LexiconSpeechoidStorageTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconStorage.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Hooks\/PlayerHooks.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Utterance\/UtteranceGeneratorTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconEntryItem.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/FlushUtterancesFromStoreByExpirationJobQueueTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentPageFactory.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Segment\/SegmentBreak.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/WikiPageTestUtil.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/.phan\/config.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/FlushUtterancesTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Hooks\/PlayerHooksTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/PreSynthesizeMessagesTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Hooks\/ApiHooksTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/ApiWikispeechListenTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Api\/ApiWikispeechListen.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/FlushUtterancesByExpirationDateOnFileTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/StandardSegmenterTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/StandardSegmenterLanguageTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/maintenance\/benchmark.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Lexicon\/LexiconWikiStorageTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Lexicon\/LexiconSpeechoidStorage.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Specials\/SpecialEditLexiconTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Segment\/LocalWikiPageProviderTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/UtteranceStore.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/SpeechoidConnector.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/FlushUtterancesFromStoreByExpirationJobTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Segment\/SegmentPageFactoryTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/CleanerTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/Lexicon\/LexiconHandlerTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Utterance\/Utterance.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/SpeechoidConnectorTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/unit\/VoiceHandlerTest.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/includes\/Specials\/SpecialEditLexicon.php":{"errors":0,"warnings":0,"messages":[]},"\/src\/repo\/tests\/phpunit\/integration\/Utterance\/UtteranceStoreTest.php":{"errors":0,"warnings":0,"messages":[]}}}

--- end ---
$ /usr/bin/composer install
--- stderr ---
Installing dependencies from lock file (including require-dev)
Verifying lock file contents can be installed on current platform.
Nothing to install, update or remove
Generating autoload files
16 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
--- stdout ---

--- end ---
$ /usr/bin/composer test
--- stderr ---
> parallel-lint . --exclude vendor --exclude node_modules
> phpcs -sp --cache
> minus-x check .
--- stdout ---
PHP 8.4.18 | 10 parallel jobs
............................................................  60/138 ( 43%)
............................................................ 120/138 ( 86%)
..................                                           138/138 (100%)


Checked 138 files in 0.5 seconds
No syntax error found
.............................................. 46 / 46 (100%)


Time: 1.94 secs; Memory: 14MB

MinusX
======
Processing /src/repo...
.............................................................
.............................................................
.............................................................
.............................................................
.............................................................
.............................................................
....................................
All good!

--- end ---
$ /usr/bin/npm audit --json
--- stdout ---
{
  "auditReportVersion": 2,
  "vulnerabilities": {
    "postcss": {
      "name": "postcss",
      "severity": "moderate",
      "isDirect": false,
      "via": [
        {
          "source": 1117015,
          "name": "postcss",
          "dependency": "postcss",
          "title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output",
          "url": "https://github.com/advisories/GHSA-qx2v-qp2m-jg93",
          "severity": "moderate",
          "cwe": [
            "CWE-79"
          ],
          "cvss": {
            "score": 6.1,
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
          },
          "range": "<8.5.10"
        }
      ],
      "effects": [],
      "range": "<8.5.10",
      "nodes": [
        "node_modules/postcss"
      ],
      "fixAvailable": true
    }
  },
  "metadata": {
    "vulnerabilities": {
      "info": 0,
      "low": 0,
      "moderate": 1,
      "high": 0,
      "critical": 0,
      "total": 1
    },
    "dependencies": {
      "prod": 1,
      "dev": 466,
      "optional": 0,
      "peer": 1,
      "peerOptional": 0,
      "total": 466
    }
  }
}

--- end ---
Attempting to npm audit fix
$ /usr/bin/npm audit fix --dry-run --only=dev --json
--- stderr ---
npm WARN invalid config only="dev" set in command line options
npm WARN invalid config Must be one of: null, prod, production
--- stdout ---
{
  "added": 0,
  "removed": 0,
  "changed": 1,
  "audited": 467,
  "funding": 109,
  "audit": {
    "auditReportVersion": 2,
    "vulnerabilities": {
      "postcss": {
        "name": "postcss",
        "severity": "moderate",
        "isDirect": false,
        "via": [
          {
            "source": 1117015,
            "name": "postcss",
            "dependency": "postcss",
            "title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output",
            "url": "https://github.com/advisories/GHSA-qx2v-qp2m-jg93",
            "severity": "moderate",
            "cwe": [
              "CWE-79"
            ],
            "cvss": {
              "score": 6.1,
              "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"
            },
            "range": "<8.5.10"
          }
        ],
        "effects": [],
        "range": "<8.5.10",
        "nodes": [
          ""
        ],
        "fixAvailable": true
      }
    },
    "metadata": {
      "vulnerabilities": {
        "info": 0,
        "low": 0,
        "moderate": 1,
        "high": 0,
        "critical": 0,
        "total": 1
      },
      "dependencies": {
        "prod": 1,
        "dev": 466,
        "optional": 0,
        "peer": 1,
        "peerOptional": 0,
        "total": 466
      }
    }
  }
}

--- end ---
{"added": 0, "removed": 0, "changed": 1, "audited": 467, "funding": 109, "audit": {"auditReportVersion": 2, "vulnerabilities": {"postcss": {"name": "postcss", "severity": "moderate", "isDirect": false, "via": [{"source": 1117015, "name": "postcss", "dependency": "postcss", "title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output", "url": "https://github.com/advisories/GHSA-qx2v-qp2m-jg93", "severity": "moderate", "cwe": ["CWE-79"], "cvss": {"score": 6.1, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}, "range": "<8.5.10"}], "effects": [], "range": "<8.5.10", "nodes": [""], "fixAvailable": true}}, "metadata": {"vulnerabilities": {"info": 0, "low": 0, "moderate": 1, "high": 0, "critical": 0, "total": 1}, "dependencies": {"prod": 1, "dev": 466, "optional": 0, "peer": 1, "peerOptional": 0, "total": 466}}}}
$ /usr/bin/npm audit fix --only=dev
--- stderr ---
npm WARN invalid config only="dev" set in command line options
npm WARN invalid config Must be one of: null, prod, production
--- stdout ---

up to date, audited 467 packages in 1s

109 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

--- end ---
Verifying that tests still pass
$ /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 466 packages, and audited 467 packages in 5s

109 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
  19:1  warning  This line has a length of 118. Maximum allowed is 100  max-len
  24:1  warning  This line has a length of 117. Maximum allowed is 100  max-len
  30:1  warning  This line has a length of 118. Maximum allowed is 100  max-len
  49:1  warning  This line has a length of 119. Maximum allowed is 100  max-len
  56:1  warning  This line has a length of 117. Maximum allowed is 100  max-len
  70:1  warning  This line has a length of 107. Maximum allowed is 100  max-len
  79:1  warning  This line has a length of 127. Maximum allowed is 100  max-len

/src/repo/modules/ext.wikispeech.feedback.js
  145:1  warning  Missing JSDoc @param "params.storage" type             jsdoc/require-param-type
  146:1  warning  Missing JSDoc @param "params.selectionPlayer" type     jsdoc/require-param-type
  220:1  warning  This line has a length of 102. Maximum allowed is 100  max-len
  228:3  warning  Prefer .then to .done                                  no-jquery/no-done-fail
  229:4  warning  Prefer .then to .done                                  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.gadget.js
  33:1  warning  Missing JSDoc @param "mainInstance" type  jsdoc/require-param-type
  39:3  warning  Prefer .then to .done                     no-jquery/no-done-fail

/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
  129:1  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.player.js
  246:3  warning  Prefer .then to .done  no-jquery/no-done-fail
  246:3  warning  Prefer .then to .fail  no-jquery/no-done-fail
  259:5  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.selectionPlayer.js
  124:3  warning  Prefer .then to .done  no-jquery/no-done-fail
  158:3  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.sharedUserOptionSettings.js
   19:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
   31:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
   31:3  warning  Prefer .then to .fail                                     no-jquery/no-done-fail
   69:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
   70:1  warning  Missing JSDoc @param "isProducer" type                    jsdoc/require-param-type
   89:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
  115:1  warning  Missing JSDoc @param "api" type                           jsdoc/require-param-type
  116:1  warning  The type 'ext.wikispeech.UserOptionsDialog' is undefined  jsdoc/no-undefined-types
  117:1  warning  Missing JSDoc @param "isProducer" type                    jsdoc/require-param-type
  133:3  warning  Prefer .then to .done                                     no-jquery/no-done-fail
  133:3  warning  Prefer .then to .fail                                     no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.storage.js
  259:19  warning  Prefer .then to .done  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.transcriptionPreviewer.js
  37:1   warning  This line has a length of 103. Maximum allowed is 100  max-len
  66:19  warning  Prefer .then to .done                                  no-jquery/no-done-fail
  66:19  warning  Prefer .then to .fail                                  no-jquery/no-done-fail

/src/repo/modules/ext.wikispeech.ui.js
   68:18  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  199:3   warning  Prefer .then to .done                                                                                no-jquery/no-done-fail
  249:1   warning  This line has a length of 107. Maximum allowed is 100                                                max-len
  331:1   warning  This line has a length of 116. Maximum allowed is 100                                                max-len
  428:30  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  436:23  warning  OOUI button has no label. Even icon-only buttons should set a label with invisibleLabel set to true  mediawiki/no-unlabeled-buttonwidget
  742:21  warning  Found non-literal argument in require                                                                security/detect-non-literal-require

/src/repo/modules/ext.wikispeech.userOptionsDialog.js
    7:1  warning  This line has a length of 102. Maximum allowed is 100  max-len
  132:3  warning  Prefer .then to .done                                  no-jquery/no-done-fail

/src/repo/tests/qunit/ext.wikispeech.ui.test.js
  67:2  warning  Prefer .then to .done  no-jquery/no-done-fail

✖ 46 problems (0 errors, 46 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 ---
{"1117015": {"source": 1117015, "name": "postcss", "dependency": "postcss", "title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output", "url": "https://github.com/advisories/GHSA-qx2v-qp2m-jg93", "severity": "moderate", "cwe": ["CWE-79"], "cvss": {"score": 6.1, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"}, "range": "<8.5.10"}}
Upgrading n:postcss from 8.5.6 -> 8.5.14
$ package-lock-lint /src/repo/package-lock.json
--- stdout ---
Checking /src/repo/package-lock.json

--- end ---
build: Updating dependencies

composer:
* mediawiki/mediawiki-codesniffer: 50.0.0 → 51.0.0

npm:
* eslint-config-wikimedia: 0.32.3 → 0.32.4
* postcss: 8.5.6 → 8.5.14
  * https://github.com/advisories/GHSA-qx2v-qp2m-jg93


$ git add .
--- stdout ---

--- end ---
$ git commit -F /tmp/tmpatyu7wwt
--- stdout ---
[master 4d4eccb] build: Updating dependencies
 4 files changed, 249 insertions(+), 183 deletions(-)

--- end ---
$ git format-patch HEAD~1 --stdout
--- stdout ---
From 4d4eccb0eed56c9cfc2c1f7a1c50eae6555f680f Mon Sep 17 00:00:00 2001
From: libraryupgrader <tools.libraryupgrader@tools.wmflabs.org>
Date: Tue, 5 May 2026 05:22:47 +0000
Subject: [PATCH] build: Updating dependencies
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

composer:
* mediawiki/mediawiki-codesniffer: 50.0.0 → 51.0.0

npm:
* eslint-config-wikimedia: 0.32.3 → 0.32.4
* postcss: 8.5.6 → 8.5.14
  * https://github.com/advisories/GHSA-qx2v-qp2m-jg93

Change-Id: Ieefb410f4df31c8b02e7a829e67faac06cc4c03f
---
 composer.json                         |   2 +-
 modules/ext.wikispeech.highlighter.js |   2 +-
 package-lock.json                     | 426 +++++++++++++++-----------
 package.json                          |   2 +-
 4 files changed, 249 insertions(+), 183 deletions(-)

diff --git a/composer.json b/composer.json
index 9678a16..75ae153 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
 {
 	"require-dev": {
-		"mediawiki/mediawiki-codesniffer": "50.0.0",
+		"mediawiki/mediawiki-codesniffer": "51.0.0",
 		"mediawiki/mediawiki-phan-config": "0.20.0",
 		"mediawiki/minus-x": "2.0.1",
 		"php-parallel-lint/php-console-highlighter": "1.0.0",
diff --git a/modules/ext.wikispeech.highlighter.js b/modules/ext.wikispeech.highlighter.js
index 1ac3a14..f170c2a 100644
--- a/modules/ext.wikispeech.highlighter.js
+++ b/modules/ext.wikispeech.highlighter.js
@@ -64,7 +64,7 @@ class Highlighter {
 			return;
 		}
 		// Class name is documented above
-		// eslint-disable-next-line mediawiki/class-doc
+
 		const span = $( '<span>' )
 			.addClass( this.utteranceHighlightingClass )
 			.get( 0 );
diff --git a/package-lock.json b/package-lock.json
index 5ce30ea..62a6ea5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
 		"": {
 			"name": "wikispeech",
 			"devDependencies": {
-				"eslint-config-wikimedia": "0.32.3",
+				"eslint-config-wikimedia": "0.32.4",
 				"grunt": "1.6.2",
 				"grunt-banana-checker": "0.13.0",
 				"grunt-eslint": "24.3.0",
@@ -245,19 +245,32 @@
 			}
 		},
 		"node_modules/@es-joy/jsdoccomment": {
-			"version": "0.76.0",
-			"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.76.0.tgz",
-			"integrity": "sha512-g+RihtzFgGTx2WYCuTHbdOXJeAlGnROws0TeALx9ow/ZmOROOZkVg5wp/B44n0WJgI4SQFP1eWM2iRPlU2Y14w==",
+			"version": "0.86.0",
+			"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz",
+			"integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==",
 			"dev": true,
 			"dependencies": {
 				"@types/estree": "^1.0.8",
-				"@typescript-eslint/types": "^8.46.0",
-				"comment-parser": "1.4.1",
-				"esquery": "^1.6.0",
-				"jsdoc-type-pratt-parser": "~6.10.0"
+				"@typescript-eslint/types": "^8.58.0",
+				"comment-parser": "1.4.6",
+				"esquery": "^1.7.0",
+				"jsdoc-type-pratt-parser": "~7.2.0"
 			},
 			"engines": {
-				"node": ">=20.11.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
+			}
+		},
+		"node_modules/@es-joy/jsdoccomment/node_modules/@typescript-eslint/types": {
+			"version": "8.59.2",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz",
+			"integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==",
+			"dev": true,
+			"engines": {
+				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+			},
+			"funding": {
+				"type": "opencollective",
+				"url": "https://opencollective.com/typescript-eslint"
 			}
 		},
 		"node_modules/@es-joy/resolve.exports": {
@@ -270,9 +283,9 @@
 			}
 		},
 		"node_modules/@eslint-community/eslint-utils": {
-			"version": "4.7.0",
-			"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
-			"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+			"version": "4.9.1",
+			"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+			"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
 			"dev": true,
 			"dependencies": {
 				"eslint-visitor-keys": "^3.4.3"
@@ -394,9 +407,9 @@
 			}
 		},
 		"node_modules/@mdn/browser-compat-data": {
-			"version": "5.7.6",
-			"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz",
-			"integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==",
+			"version": "6.1.5",
+			"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-6.1.5.tgz",
+			"integrity": "sha512-PzdZZzRhcXvKB0begee28n5lvwAcinGKYuLZOVxHAZm+n7y01ddEGfdS1ZXRuVcV+ndG6mSEAE8vgudom5UjYg==",
 			"dev": true
 		},
 		"node_modules/@nodelib/fs.scandir": {
@@ -593,20 +606,19 @@
 			"dev": true
 		},
 		"node_modules/@typescript-eslint/eslint-plugin": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
-			"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
+			"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
 			"dev": true,
 			"dependencies": {
-				"@eslint-community/regexpp": "^4.10.0",
-				"@typescript-eslint/scope-manager": "8.46.0",
-				"@typescript-eslint/type-utils": "8.46.0",
-				"@typescript-eslint/utils": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0",
-				"graphemer": "^1.4.0",
-				"ignore": "^7.0.0",
+				"@eslint-community/regexpp": "^4.12.2",
+				"@typescript-eslint/scope-manager": "8.54.0",
+				"@typescript-eslint/type-utils": "8.54.0",
+				"@typescript-eslint/utils": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0",
+				"ignore": "^7.0.5",
 				"natural-compare": "^1.4.0",
-				"ts-api-utils": "^2.1.0"
+				"ts-api-utils": "^2.4.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -616,7 +628,7 @@
 				"url": "https://opencollective.com/typescript-eslint"
 			},
 			"peerDependencies": {
-				"@typescript-eslint/parser": "^8.46.0",
+				"@typescript-eslint/parser": "^8.54.0",
 				"eslint": "^8.57.0 || ^9.0.0",
 				"typescript": ">=4.8.4 <6.0.0"
 			}
@@ -631,16 +643,16 @@
 			}
 		},
 		"node_modules/@typescript-eslint/parser": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
-			"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
+			"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/scope-manager": "8.46.0",
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/typescript-estree": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0",
-				"debug": "^4.3.4"
+				"@typescript-eslint/scope-manager": "8.54.0",
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/typescript-estree": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0",
+				"debug": "^4.4.3"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -655,14 +667,14 @@
 			}
 		},
 		"node_modules/@typescript-eslint/project-service": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
-			"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
+			"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/tsconfig-utils": "^8.46.0",
-				"@typescript-eslint/types": "^8.46.0",
-				"debug": "^4.3.4"
+				"@typescript-eslint/tsconfig-utils": "^8.54.0",
+				"@typescript-eslint/types": "^8.54.0",
+				"debug": "^4.4.3"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -676,13 +688,13 @@
 			}
 		},
 		"node_modules/@typescript-eslint/scope-manager": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
-			"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
+			"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0"
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -693,9 +705,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/tsconfig-utils": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
-			"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
+			"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
 			"dev": true,
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -709,16 +721,16 @@
 			}
 		},
 		"node_modules/@typescript-eslint/type-utils": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
-			"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
+			"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/typescript-estree": "8.46.0",
-				"@typescript-eslint/utils": "8.46.0",
-				"debug": "^4.3.4",
-				"ts-api-utils": "^2.1.0"
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/typescript-estree": "8.54.0",
+				"@typescript-eslint/utils": "8.54.0",
+				"debug": "^4.4.3",
+				"ts-api-utils": "^2.4.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -733,9 +745,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/types": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
-			"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
+			"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
 			"dev": true,
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -746,21 +758,20 @@
 			}
 		},
 		"node_modules/@typescript-eslint/typescript-estree": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
-			"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
+			"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/project-service": "8.46.0",
-				"@typescript-eslint/tsconfig-utils": "8.46.0",
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/visitor-keys": "8.46.0",
-				"debug": "^4.3.4",
-				"fast-glob": "^3.3.2",
-				"is-glob": "^4.0.3",
-				"minimatch": "^9.0.4",
-				"semver": "^7.6.0",
-				"ts-api-utils": "^2.1.0"
+				"@typescript-eslint/project-service": "8.54.0",
+				"@typescript-eslint/tsconfig-utils": "8.54.0",
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/visitor-keys": "8.54.0",
+				"debug": "^4.4.3",
+				"minimatch": "^9.0.5",
+				"semver": "^7.7.3",
+				"tinyglobby": "^0.2.15",
+				"ts-api-utils": "^2.4.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -774,9 +785,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
-			"version": "2.0.3",
-			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
-			"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
+			"version": "2.1.0",
+			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+			"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
 			"dev": true,
 			"dependencies": {
 				"balanced-match": "^1.0.0"
@@ -798,15 +809,15 @@
 			}
 		},
 		"node_modules/@typescript-eslint/utils": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
-			"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
+			"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
 			"dev": true,
 			"dependencies": {
-				"@eslint-community/eslint-utils": "^4.7.0",
-				"@typescript-eslint/scope-manager": "8.46.0",
-				"@typescript-eslint/types": "8.46.0",
-				"@typescript-eslint/typescript-estree": "8.46.0"
+				"@eslint-community/eslint-utils": "^4.9.1",
+				"@typescript-eslint/scope-manager": "8.54.0",
+				"@typescript-eslint/types": "8.54.0",
+				"@typescript-eslint/typescript-estree": "8.54.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -821,12 +832,12 @@
 			}
 		},
 		"node_modules/@typescript-eslint/visitor-keys": {
-			"version": "8.46.0",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
-			"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
+			"version": "8.54.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
+			"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.46.0",
+				"@typescript-eslint/types": "8.54.0",
 				"eslint-visitor-keys": "^4.2.1"
 			},
 			"engines": {
@@ -872,9 +883,9 @@
 			"dev": true
 		},
 		"node_modules/acorn": {
-			"version": "8.15.0",
-			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
-			"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+			"version": "8.16.0",
+			"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+			"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
 			"dev": true,
 			"bin": {
 				"acorn": "bin/acorn"
@@ -1004,6 +1015,12 @@
 				"@mdn/browser-compat-data": "^5.6.19"
 			}
 		},
+		"node_modules/ast-metadata-inferer/node_modules/@mdn/browser-compat-data": {
+			"version": "5.7.6",
+			"resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.7.6.tgz",
+			"integrity": "sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==",
+			"dev": true
+		},
 		"node_modules/astral-regex": {
 			"version": "2.0.0",
 			"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@@ -1263,9 +1280,9 @@
 			}
 		},
 		"node_modules/comment-parser": {
-			"version": "1.4.1",
-			"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
-			"integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==",
+			"version": "1.4.6",
+			"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz",
+			"integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==",
 			"dev": true,
 			"engines": {
 				"node": ">= 12.0.0"
@@ -1612,13 +1629,13 @@
 			}
 		},
 		"node_modules/enhanced-resolve": {
-			"version": "5.18.3",
-			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
-			"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+			"version": "5.21.0",
+			"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz",
+			"integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==",
 			"dev": true,
 			"dependencies": {
 				"graceful-fs": "^4.2.4",
-				"tapable": "^2.2.0"
+				"tapable": "^2.3.3"
 			},
 			"engines": {
 				"node": ">=10.13.0"
@@ -1746,46 +1763,47 @@
 			}
 		},
 		"node_modules/eslint-config-wikimedia": {
-			"version": "0.32.3",
-			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.32.3.tgz",
-			"integrity": "sha512-Ekz2/ozpCCjQl3VbC6dW7ChqoW7FRilLDxmJ+FJOZhIxxzZSZR5QqQOAGWSZAlG1ONkZbYV/TPwGLWZcrNxyaA==",
+			"version": "0.32.4",
+			"resolved": "https://registry.npmjs.org/eslint-config-wikimedia/-/eslint-config-wikimedia-0.32.4.tgz",
+			"integrity": "sha512-zcHJYss2vo8HK5PzkFuaV9mzaSGRuhA+jFGoQ4rNIwWz0usZsuQ2LYpkKxrbCVX1CbV0PzG+jJ6p0cLI+G37JQ==",
 			"dev": true,
 			"dependencies": {
 				"@stylistic/eslint-plugin": "^3.1.0",
-				"@typescript-eslint/eslint-plugin": "8.46.0",
-				"@typescript-eslint/parser": "8.46.0",
+				"@typescript-eslint/eslint-plugin": "8.54.0",
+				"@typescript-eslint/parser": "8.54.0",
 				"browserslist-config-wikimedia": "^0.7.0",
-				"eslint": "^8.57.0",
-				"eslint-plugin-compat": "^6.0.2",
+				"eslint-plugin-compat": "^6.1.0",
 				"eslint-plugin-es-x": "^8.7.0",
-				"eslint-plugin-jest": "^29.0.1",
-				"eslint-plugin-jsdoc": "61.3.0",
+				"eslint-plugin-jest": "^29.12.2",
+				"eslint-plugin-jsdoc": "^62.9.0",
 				"eslint-plugin-json-es": "^1.6.0",
-				"eslint-plugin-mediawiki": "^0.8.2",
+				"eslint-plugin-mediawiki": "^0.8.3",
 				"eslint-plugin-mocha": "^10.5.0",
-				"eslint-plugin-n": "^17.23.1",
-				"eslint-plugin-no-jquery": "^3.1.1",
-				"eslint-plugin-qunit": "^8.2.5",
-				"eslint-plugin-security": "^3.0.1",
+				"eslint-plugin-n": "^17.24.0",
+				"eslint-plugin-no-jquery": "^4.0.0",
+				"eslint-plugin-qunit": "^8.2.6",
+				"eslint-plugin-security": "^4.0.0",
 				"eslint-plugin-unicorn": "^56.0.1",
 				"eslint-plugin-vue": "^9.33.0",
-				"eslint-plugin-wdio": "^9.16.2",
+				"eslint-plugin-wdio": "9.23.0",
 				"eslint-plugin-yml": "^1.19.0"
 			},
 			"engines": {
 				"node": ">=20 <25"
+			},
+			"peerDependencies": {
+				"eslint": "^8.57.0"
 			}
 		},
 		"node_modules/eslint-plugin-compat": {
-			"version": "6.0.2",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.0.2.tgz",
-			"integrity": "sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==",
+			"version": "6.2.1",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-6.2.1.tgz",
+			"integrity": "sha512-gLKqUH+lQcCL+HzsROUjBDvakc5Zaga51Y4ZAkPCXc41pzKBfyluqTr2j8zOx8QQQb7zyglu1LVoL5aSNWf2SQ==",
 			"dev": true,
 			"dependencies": {
-				"@mdn/browser-compat-data": "^5.5.35",
+				"@mdn/browser-compat-data": "^6.1.1",
 				"ast-metadata-inferer": "^0.8.1",
-				"browserslist": "^4.24.2",
-				"caniuse-lite": "^1.0.30001687",
+				"browserslist": "^4.25.2",
 				"find-up": "^5.0.0",
 				"globals": "^15.7.0",
 				"lodash.memoize": "^4.1.2",
@@ -1795,7 +1813,7 @@
 				"node": ">=18.x"
 			},
 			"peerDependencies": {
-				"eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+				"eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
 			}
 		},
 		"node_modules/eslint-plugin-compat/node_modules/globals": {
@@ -1861,57 +1879,57 @@
 			}
 		},
 		"node_modules/eslint-plugin-jsdoc": {
-			"version": "61.3.0",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-61.3.0.tgz",
-			"integrity": "sha512-E4m/5J5lrasd63Z74q4CCZ4PFnywnnrcvA7zZ98802NPhrZKKTp5NH+XAT+afcjXp2ps2/OQF5gPSWCT2XFCJg==",
+			"version": "62.9.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz",
+			"integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==",
 			"dev": true,
 			"dependencies": {
-				"@es-joy/jsdoccomment": "~0.76.0",
+				"@es-joy/jsdoccomment": "~0.86.0",
 				"@es-joy/resolve.exports": "1.2.0",
 				"are-docs-informative": "^0.0.2",
-				"comment-parser": "1.4.1",
+				"comment-parser": "1.4.6",
 				"debug": "^4.4.3",
 				"escape-string-regexp": "^4.0.0",
-				"espree": "^10.4.0",
-				"esquery": "^1.6.0",
+				"espree": "^11.2.0",
+				"esquery": "^1.7.0",
 				"html-entities": "^2.6.0",
 				"object-deep-merge": "^2.0.0",
 				"parse-imports-exports": "^0.2.4",
-				"semver": "^7.7.3",
+				"semver": "^7.7.4",
 				"spdx-expression-parse": "^4.0.0",
 				"to-valid-identifier": "^1.0.0"
 			},
 			"engines": {
-				"node": ">=20.11.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
 			},
 			"peerDependencies": {
-				"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+				"eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
 			}
 		},
 		"node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": {
-			"version": "4.2.1",
-			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
-			"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+			"version": "5.0.1",
+			"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+			"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
 			"dev": true,
 			"engines": {
-				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
 			},
 			"funding": {
 				"url": "https://opencollective.com/eslint"
 			}
 		},
 		"node_modules/eslint-plugin-jsdoc/node_modules/espree": {
-			"version": "10.4.0",
-			"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
-			"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+			"version": "11.2.0",
+			"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+			"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
 			"dev": true,
 			"dependencies": {
-				"acorn": "^8.15.0",
+				"acorn": "^8.16.0",
 				"acorn-jsx": "^5.3.2",
-				"eslint-visitor-keys": "^4.2.1"
+				"eslint-visitor-keys": "^5.0.1"
 			},
 			"engines": {
-				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+				"node": "^20.19.0 || ^22.13.0 || >=24"
 			},
 			"funding": {
 				"url": "https://opencollective.com/eslint"
@@ -1931,9 +1949,9 @@
 			}
 		},
 		"node_modules/eslint-plugin-mediawiki": {
-			"version": "0.8.2",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.8.2.tgz",
-			"integrity": "sha512-ydYrpkzm8IVVDQA96QPF3HnFd2xjkIEh7gixD2gvOqUbUZF0p36LtpWXOFAlPWAvHLePWbNNTD5ovd3d4hEtog==",
+			"version": "0.8.3",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-mediawiki/-/eslint-plugin-mediawiki-0.8.3.tgz",
+			"integrity": "sha512-RQKZd40C1taMDk5N9+aFLEBGBB95RNG7Gc54EsJ8pHsJu8//nIdpxNFWPtQz6RNxz6pZUXBnMCxzkMOLM3Mm1w==",
 			"dev": true,
 			"dependencies": {
 				"upath": "^2.0.1"
@@ -1960,9 +1978,9 @@
 			}
 		},
 		"node_modules/eslint-plugin-n": {
-			"version": "17.23.1",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.23.1.tgz",
-			"integrity": "sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==",
+			"version": "17.24.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.24.0.tgz",
+			"integrity": "sha512-/gC7/KAYmfNnPNOb3eu8vw+TdVnV0zhdQwexsw6FLXbhzroVj20vRn2qL8lDWDGnAQ2J8DhdfvXxX9EoxvERvw==",
 			"dev": true,
 			"dependencies": {
 				"@eslint-community/eslint-utils": "^4.5.0",
@@ -2034,31 +2052,34 @@
 			}
 		},
 		"node_modules/eslint-plugin-no-jquery": {
-			"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==",
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-4.0.0.tgz",
+			"integrity": "sha512-ZR631D3qIQfgjKOAcgvYa5cB8xdTvFXAD5MbK5x5WltLSwFxmGnoaTXNtnptFU7py07ALrIe5dZRYncu4RD/Ug==",
 			"dev": true,
 			"peerDependencies": {
-				"eslint": ">=8.0.0"
+				"eslint": ">=8.0.0 <9.0.0"
 			}
 		},
 		"node_modules/eslint-plugin-qunit": {
-			"version": "8.2.5",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-qunit/-/eslint-plugin-qunit-8.2.5.tgz",
-			"integrity": "sha512-qr7RJCYImKQjB+39q4q46i1l7p1V3joHzBE5CAYfxn5tfVFjrnjn/tw7q/kDyweU9kAIcLul0Dx/KWVUCb3BgA==",
+			"version": "8.2.6",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-qunit/-/eslint-plugin-qunit-8.2.6.tgz",
+			"integrity": "sha512-S1jC/DIW9J8VtNX4uG1vlf5FZVrfQFlcuiYmvTHR2IICUhubHqpWA5o+qS1tujh+81Gs39omKV2D4OXfbSJE5g==",
 			"dev": true,
 			"dependencies": {
-				"eslint-utils": "^3.0.0",
+				"@eslint-community/eslint-utils": "^4.4.0",
 				"requireindex": "^1.2.0"
 			},
 			"engines": {
 				"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
+			},
+			"peerDependencies": {
+				"eslint": ">=8.38.0"
 			}
 		},
 		"node_modules/eslint-plugin-security": {
-			"version": "3.0.1",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz",
-			"integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==",
+			"version": "4.0.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.0.tgz",
+			"integrity": "sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==",
 			"dev": true,
 			"dependencies": {
 				"safe-regex": "^2.1.1"
@@ -2138,9 +2159,9 @@
 			}
 		},
 		"node_modules/eslint-plugin-wdio": {
-			"version": "9.16.2",
-			"resolved": "https://registry.npmjs.org/eslint-plugin-wdio/-/eslint-plugin-wdio-9.16.2.tgz",
-			"integrity": "sha512-qkqsPgxN70OnUPWMjmzJbSbvm2+Q087JIGss53/OFI4Y46xKlV5VLhLiYealaAibAiXmnfWKd0tERjZAzVL87A==",
+			"version": "9.23.0",
+			"resolved": "https://registry.npmjs.org/eslint-plugin-wdio/-/eslint-plugin-wdio-9.23.0.tgz",
+			"integrity": "sha512-8tcpupzp2Qmv+uSfhzeHi42LVA9PyjkpMBPclSIkPxBfXpj4fMrejwAHu1PROh1OmJN1VQcGQUTWvSzyRcV2vA==",
 			"dev": true,
 			"engines": {
 				"node": ">=18.20.0"
@@ -2285,9 +2306,9 @@
 			}
 		},
 		"node_modules/esquery": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
-			"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+			"version": "1.7.0",
+			"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+			"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
 			"dev": true,
 			"dependencies": {
 				"estraverse": "^5.1.0"
@@ -2582,9 +2603,9 @@
 			}
 		},
 		"node_modules/get-tsconfig": {
-			"version": "4.13.0",
-			"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
-			"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
+			"version": "4.14.0",
+			"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+			"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
 			"dev": true,
 			"dependencies": {
 				"resolve-pkg-maps": "^1.0.0"
@@ -3295,9 +3316,9 @@
 			"dev": true
 		},
 		"node_modules/jsdoc-type-pratt-parser": {
-			"version": "6.10.0",
-			"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-6.10.0.tgz",
-			"integrity": "sha512-+LexoTRyYui5iOhJGn13N9ZazL23nAHGkXsa1p/C8yeq79WRfLBag6ZZ0FQG2aRoc9yfo59JT9EYCQonOkHKkQ==",
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz",
+			"integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==",
 			"dev": true,
 			"engines": {
 				"node": ">=20.0.0"
@@ -4087,9 +4108,9 @@
 			}
 		},
 		"node_modules/postcss": {
-			"version": "8.5.6",
-			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
-			"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+			"version": "8.5.14",
+			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+			"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
 			"dev": true,
 			"funding": [
 				{
@@ -4607,9 +4628,9 @@
 			"dev": true
 		},
 		"node_modules/semver": {
-			"version": "7.7.3",
-			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
-			"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+			"version": "7.7.4",
+			"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+			"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
 			"dev": true,
 			"bin": {
 				"semver": "bin/semver.js"
@@ -5169,9 +5190,9 @@
 			"dev": true
 		},
 		"node_modules/tapable": {
-			"version": "2.3.0",
-			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
-			"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+			"version": "2.3.3",
+			"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+			"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
 			"dev": true,
 			"engines": {
 				"node": ">=6"
@@ -5187,6 +5208,51 @@
 			"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
 			"dev": true
 		},
+		"node_modules/tinyglobby": {
+			"version": "0.2.16",
+			"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+			"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+			"dev": true,
+			"dependencies": {
+				"fdir": "^6.5.0",
+				"picomatch": "^4.0.4"
+			},
+			"engines": {
+				"node": ">=12.0.0"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/SuperchupuDev"
+			}
+		},
+		"node_modules/tinyglobby/node_modules/fdir": {
+			"version": "6.5.0",
+			"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+			"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+			"dev": true,
+			"engines": {
+				"node": ">=12.0.0"
+			},
+			"peerDependencies": {
+				"picomatch": "^3 || ^4"
+			},
+			"peerDependenciesMeta": {
+				"picomatch": {
+					"optional": true
+				}
+			}
+		},
+		"node_modules/tinyglobby/node_modules/picomatch": {
+			"version": "4.0.4",
+			"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+			"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+			"dev": true,
+			"engines": {
+				"node": ">=12"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/jonschlinkert"
+			}
+		},
 		"node_modules/to-regex-range": {
 			"version": "5.0.1",
 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -5216,9 +5282,9 @@
 			}
 		},
 		"node_modules/ts-api-utils": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
-			"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+			"version": "2.5.0",
+			"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+			"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
 			"dev": true,
 			"engines": {
 				"node": ">=18.12"
diff --git a/package.json b/package.json
index ae51d19..8cf0146 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
 		"doc": "jsdoc -c jsdoc.json"
 	},
 	"devDependencies": {
-		"eslint-config-wikimedia": "0.32.3",
+		"eslint-config-wikimedia": "0.32.4",
 		"grunt": "1.6.2",
 		"grunt-banana-checker": "0.13.0",
 		"grunt-eslint": "24.3.0",
-- 
2.47.3


--- end ---
Source code is licensed under the AGPL.