diff --git a/.gitignore b/.gitignore index febbb5c96..6288b5e5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /node_modules /build +/script/fluent-bundle.js +/src/services/locale-imports.ts + diff --git a/.prettierignore b/.prettierignore index 900872f5f..a9008016f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,7 @@ *.min.js *.min.css build +script/fluent-bundle.js src/intro.js src/outro.js test/support/jquery-1.5.2.js \ No newline at end of file diff --git a/Makefile b/Makefile index 0dba6b729..e05ba37dd 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,9 @@ BASE_SOURCES = \ $(SRC_DIR)/unicode.ts \ $(SRC_DIR)/browser.ts \ $(SRC_DIR)/animate.ts \ + $(FLUENT_BUNDLE) \ + $(SRC_DIR)/services/locale-imports.ts \ + $(SRC_DIR)/services/localization.ts \ $(SRC_DIR)/services/aria.ts \ $(SRC_DIR)/domFragment.ts \ $(SRC_DIR)/tree.ts \ @@ -101,6 +104,7 @@ endif # http://www.gnu.org/software/make/manual/html_node/Empty-Targets.html#Empty-Targets NODE_MODULES_INSTALLED = ./node_modules/.installed--used_by_Makefile BUILD_DIR_EXISTS = $(BUILD_DIR)/.exists--used_by_Makefile +FLUENT_BUNDLE = script/fluent-bundle.js # environment constants @@ -120,6 +124,7 @@ css: $(BUILD_CSS) font: $(FONT_TARGET) clean: rm -rf $(BUILD_DIR) + rm -f $(SRC_DIR)/services/locale-imports.ts # This adds an entry to your local .git/config file that looks like this: # [include] # path = ../.gitconfig @@ -129,7 +134,7 @@ setup-gitconfig: prettify-all: npx prettier --write '**/*.{ts,js,css,html}' -$(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) +$(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) $(FLUENT_BUNDLE) cat $^ | ./script/escape-non-ascii | ./script/tsc-emit-only > $@ perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ @@ -137,7 +142,7 @@ $(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED) $(UGLIFY) $(UGLIFY_OPTS) < $< > $@ -$(BASIC_JS): $(INTRO) $(SOURCES_BASIC) $(OUTRO) $(BUILD_DIR_EXISTS) +$(BASIC_JS): $(INTRO) $(SOURCES_BASIC) $(OUTRO) $(BUILD_DIR_EXISTS) $(FLUENT_BUNDLE) cat $^ | ./script/escape-non-ascii | ./script/tsc-emit-only > $@ perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ @@ -168,6 +173,29 @@ $(FONT_TARGET): $(FONT_SOURCE) $(BUILD_DIR_EXISTS) rm -rf $@ cp -r $< $@ +$(FLUENT_BUNDLE): package.json script/bundle-fluent.js $(NODE_MODULES_INSTALLED) + @echo "Generating fluent-bundle.js..." + @node -e " \ + const fs = require('fs'); \ + const path = require('path'); \ + const { execSync } = require('child_process'); \ + console.log('📦 Bundling Fluent libraries for MathQuill...'); \ + const entryFile = \`const { FluentBundle, FluentResource } = require('@fluent/bundle'); const { parse } = require('@fluent/syntax'); module.exports = { FluentBundle, FluentResource, parseResource: parse };\`; \ + fs.writeFileSync('temp-fluent-entry.js', entryFile); \ + const webpackConfig = \`const path = require('path'); module.exports = { mode: 'production', entry: './temp-fluent-entry.js', output: { path: path.resolve(__dirname, 'script'), filename: 'fluent-bundle.js', library: { name: 'FluentLib', type: 'var' } }, target: 'web' };\`; \ + fs.writeFileSync('temp-webpack.config.js', webpackConfig); \ + try { execSync('npx webpack --config temp-webpack.config.js', {stdio: 'inherit'}); } catch(e) { console.error('Webpack failed:', e.message); process.exit(1); } \ + const globalExports = '\\nvar FluentBundle = FluentLib.FluentBundle;\\nvar FluentResource = FluentLib.FluentResource;\\nvar parseResource = FluentLib.parseResource;\\n'; \ + fs.appendFileSync('script/fluent-bundle.js', globalExports); \ + fs.unlinkSync('temp-fluent-entry.js'); \ + fs.unlinkSync('temp-webpack.config.js'); \ + console.log('✅ Fluent bundle generated successfully'); \ + console.log('📍 Location: script/fluent-bundle.js'); \ + " + +$(SRC_DIR)/services/locale-imports.ts: script/generate-locale-imports.js src/locale/*/messages.ftl + node script/generate-locale-imports.js + # # -*- Test tasks -*- # @@ -187,6 +215,6 @@ benchmark: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) @echo @echo "** now open benchmark/{render,select,update}.html in your browser. **" -$(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(TEST_SUPPORT) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS) +$(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(TEST_SUPPORT) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS) $(FLUENT_BUNDLE) cat $^ | ./script/tsc-emit-only > $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md index 78dc26818..9fef5d9f4 100644 --- a/docs/Api_Methods.md +++ b/docs/Api_Methods.md @@ -72,7 +72,7 @@ Updates the default [configuration options](Config.md) for this instance of the If there are multiple instances of the MathQuill API, `MQ.config()` only affects the math MathQuill objects created by `MQ`. E.g.: -```javascript +```js var MQ1 = MathQuill.getInterface(3), MQ2 = MathQuill.getInterface(3); @@ -216,7 +216,7 @@ Removes focus from the editable field. Write the given LaTeX at the current cursor position. If the cursor does not have focus, writes to last position the cursor occupied in the editable field. -```javascript +```js mathField.write(' - 1'); // writes ' - 1' to mathField at the cursor position ``` @@ -224,7 +224,7 @@ mathField.write(' - 1'); // writes ' - 1' to mathField at the cursor position Enter a LaTeX command at the current cursor position or with the current selection. If the cursor does not have focus, it writes it to last position the cursor occupied in the editable field. -```javascript +```js mathField.cmd('\\sqrt'); // writes a square root command at the cursor position ``` @@ -244,7 +244,7 @@ Move the cursor to the left/right end of the editable field, respectively. These Moves the cursor to the end of the mathfield in the direction specified. The direction can be one of `MQ.L` or `MQ.R`. These are constants, where `MQ.L === -MQ.R` and vice versa. This function may be easier to use than [moveToLeftEnd or moveToRightEnd](#movetoleftend-movetorightend) if used in the [`moveOutOf` handler](Config.md#outof-handlers). -```javascript +```js var config = { handlers: { moveOutOf: function(direction) { @@ -258,7 +258,7 @@ var config = { Simulates keystrokes given a string like `"Ctrl-Home Del"`, a whitespace-delimited list of [key inputs](http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#fixed-virtual-key-codes) with optional prefixes. -```javascript +```js mathField.keystroke('Shift-Left'); // Selects character before the current cursor position ``` @@ -266,18 +266,18 @@ mathField.keystroke('Shift-Left'); // Selects character before the current curso Simulates typing text, one character at a time from where the cursor currently is. This is supposed to be identical to what would happen if a user were typing the text in. -```javascript +```js // Types part of the demo from mathquill.com without delays between keystrokes mathField.typedText('x=-b\\pm \\sqrt b^2 -4ac'); ``` ## .setAriaLabel(ariaLabel) -Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'Math Input'` +Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'Math Input'` for English or `'Entrada Matemática'` for Spanish ## .getAriaLabel() -Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'Math Input'` is returned. +Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, the default is returned (`'Math Input'` for English, `'Entrada Matemática'` for Spanish). ## .setAriaPostLabel(ariaPostLabel, timeout) @@ -293,7 +293,7 @@ Returns the suffix to be appended to the [ARIA label][`aria-label`], after the m Returns `true` if the user is currently selecting text with the mouse, `false` otherwise. This can be useful for preventing certain actions (like setting the cursor position) while the user is actively dragging to select text. The method tracks mouse selection from the moment the user presses the mouse button down to start selecting until they release it or the selection is cancelled due to an edit operation. -```javascript +```js if (!mathField.isUserSelecting()) { // Safe to programmatically change cursor position mathField.moveToLeftEnd(); @@ -307,6 +307,123 @@ if (!mathField.isUserSelecting()) { Changes the [configuration](Config.md) of just this math field. +## Language Support + +MathQuill supports internationalization through the `language` configuration option. This affects how mathematical expressions are read aloud for screen readers and accessibility tools. + +### Setting Language + +You can set the language when creating a MathField: + +```js +var mathField = MQ.MathField(document.getElementById('math-input'), { + language: 'es' // Spanish +}); +``` + +Or change it later using the config method: + +```js +mathField.config({ language: 'en' }); // Switch to English +``` + +### Supported Languages + +Currently supported language codes: + +- `'en'` - English (default) +- `'es'` - Spanish (Español) + +### Language Features + +The language setting affects how mathematical expressions are read aloud by screen readers. This includes operators, inequalities, fractions, powers, mathematical structures, and function names. For example, `sin(x^2) ≤ 1/2` reads as "sine of x squared less than or equal to 1 half" in English or "seno de x al cuadrado menor o igual que 1 medio" in Spanish. + +### Language Validation and Fallback + +MathQuill provides graceful fallback for unsupported languages. When an unsupported language is specified, it will automatically fall back to the closest supported language or English as a final fallback: + +```js +// Language variants are resolved automatically +var mathField = MQ.MathField(element, { language: 'en-US' }); // Uses 'en' +var mathField = MQ.MathField(element, { language: 'es-MX' }); // Uses 'es' + +// Unsupported languages fall back to English with a console warning +var mathField = MQ.MathField(element, { language: 'fr' }); // Falls back to 'en', logs warning +``` + +The fallback system works for both initial configuration and runtime language changes: + +```js +mathField.config({ language: 'fr-CA' }); // Falls back to 'en', logs warning +``` + +### Examples + +```js +// English math field (default) +var englishField = MQ.MathField(document.getElementById('english-math')); + +// Spanish math field +var spanishField = MQ.MathField(document.getElementById('spanish-math'), { + language: 'es' +}); + +// Global default language +MQ.config({ language: 'es' }); // All new fields will default to Spanish +``` + +### Localization API + +MathQuill provides a localization API accessible through `MQ.L10N` for working with languages and translations: + +```js +// Check if a language is supported +var isSupported = MQ.L10N.isLanguageSupported('es'); // true + +// Resolve language codes (handles variants like 'en-US' → 'en') +var resolved = MQ.L10N.resolveLanguage('en-US'); // returns 'en' + +// Set global default language for new MathQuill instances +MQ.L10N.setLanguage('es'); + +// Get current global default language +var currentLang = MQ.L10N.getLanguage(); // returns current language + +// Listen for global language changes +var unsubscribe = MQ.L10N.onLanguageChange(function() { + console.log('Language changed to:', MQ.L10N.getLanguage()); +}); + +// Later, unsubscribe from changes +unsubscribe(); + +// Create a localization instance for custom use (advanced) +var localization = MQ.L10N.create('es'); +var translation = localization.formatMessage('plus'); // 'más' +``` + +#### MQ.L10N.onLanguageChange(callback) + +Registers a callback function that will be called whenever the global default language changes. This is useful for updating UI elements or re-rendering content when the language changes. + +```js +// Listen for language changes +var unsubscribe = MQ.L10N.onLanguageChange(function() { + // This will be called whenever MQ.L10N.setLanguage() is called + var newLanguage = MQ.L10N.getLanguage(); + console.log('Language changed to:', newLanguage); + + // Update your UI or re-render content as needed +}); + +// To stop listening for changes, call the returned function +unsubscribe(); +``` + +The callback receives no arguments. Use `MQ.L10N.getLanguage()` to get the current language within the callback. + +**Returns:** A function that can be called to unregister the callback. + ## .dropEmbedded(pageX, pageY, options) **[ᴇxᴘᴇʀɪᴍᴇɴᴛᴀʟ](#note-on-experimental-features)** Insert a custom embedded element at the given coordinates, where `options` is an object like: diff --git a/docs/Config.md b/docs/Config.md index 9a61397d6..01b9b6fed 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -4,6 +4,7 @@ The configuration options object is of the following form: ```js { + language: 'en', spaceBehavesLikeTab: true, leftRightIntoCmdGoes: 'up', restrictMismatchedBrackets: true, @@ -32,6 +33,35 @@ Defaults may be set with [`MQ.config(global_config)`](Api_Methods.md#mqconfigcon # Configuration Options +## language + +The `language` option sets the language for mathematical speech and accessibility features. This affects how screen readers and other accessibility tools announce mathematical expressions. + +**Type**: `string` +**Default**: `'en'` +**Supported values**: `'en'` (English), `'es'` (Spanish) + +```js +// Set language when creating a MathField +var mathField = MQ.MathField(element, { language: 'es' }); + +// Change language later +mathField.config({ language: 'en' }); + +// Set global default language +MQ.config({ language: 'es' }); +``` + +### Language Features + +The language setting affects how mathematical expressions are read aloud by screen readers. Operators (`+`, `-`, `=`), inequalities (`<`, `≤`), fractions (`1/2` becomes "1 half"), powers (`x^2` becomes "x squared"), mathematical structures (roots, summations), and function names (`sin`, `log`) are all localized. For example, `x^2 = 1/2` reads as "x squared equals 1 half" in English or "x al cuadrado igual 1 medio" in Spanish. + +### Language Validation + +- When creating a MathField with an invalid language code, an error is thrown +- When using `.config()` to change language, invalid codes fall back to English with a console warning +- Language codes are case-insensitive and support fallbacks (e.g., `'en-US'` falls back to `'en'`) + ## spacesBehavesLikeTab If `spaceBehavesLikeTab` is true the keystrokes `{Shift-,}Spacebar` will behave like `{Shift-,}Tab` escaping from the current block (as opposed to the default behavior of inserting a Space character). @@ -84,6 +114,8 @@ Just like [`autoCommands`](#autocommands) above, this takes a string formatted a autoOperatorNames can also accept a speech-friendly alternative for each operator name. This will get read out by screenreaders in place of the raw command. To specify a speech-friendly-alternative, add a `|` character after the command, and then add the speech-friendly-alternative as a string, with spaces replaced by `-`. E.g. `stdev|standard-deviation`. +Built-in operator names are automatically localized for screen readers based on the [`language`](#language) setting. For example, `sin` is read as "sine" in English or "seno" in Spanish. Custom speech alternatives using the `|` syntax take precedence over automatic localization. + ## infixOperatorNames `infixOperatorNames` specifies a set of operator names that should be treated as infix operators, for example for determining when to stop scanning left before a fraction. diff --git a/package-lock.json b/package-lock.json index 832445f35..6d8571566 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,22 +8,2195 @@ "name": "mathquill", "version": "0.10.1", "license": "MPL-2.0", + "dependencies": { + "@fluent/bundle": "^0.19.1", + "@fluent/syntax": "^0.19.0", + "jsdom": "^26.1.0" + }, "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/preset-env": "^7.28.0", "@types/mocha": ">=5.2.7", + "babel-loader": "^10.0.0", "husky": ">=9.0", "less": ">=4.0", "lint-staged": ">=12.3.1", "mocha": ">=2.4.1", "prettier": "~3.2.0", "typescript": "~5.4", - "uglify-js": "2.x" + "uglify-js": "2.x", + "webpack": "^5.100.0", + "webpack-cli": "^6.0.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", + "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.0.tgz", + "integrity": "sha512-LOAozRVbqxEVjSKfhGnuLoE4Kz4Oc5UJzuvFUhSsQzdCdaAQu06mG8zDv2GFSerM62nImUZ7K92vxnQcLSDlCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", + "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.0", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.0", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@fluent/bundle": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@fluent/bundle/-/bundle-0.19.1.tgz", + "integrity": "sha512-SWJLZrPamDPsJlFFOW1nkgN0j0rbPbmSdmK0XAoXlyqKieLtMVl4vzng3aR5pwKoUx0scug8+YY2oct3fdfy9A==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@fluent/syntax": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@fluent/syntax/-/syntax-0.19.0.tgz", + "integrity": "sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", + "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@types/mocha": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", - "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", - "dev": true + "node_modules/acorn-import-phases": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", + "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } }, "node_modules/align-text": { "version": "0.1.4", @@ -109,6 +2282,75 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/babel-loader": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", + "integrity": "sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": "^18.20.0 || ^20.10.0 || >=22.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5.61.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -154,6 +2396,46 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -166,6 +2448,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -231,6 +2534,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -323,6 +2636,31 @@ "node": ">=8" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -356,6 +2694,13 @@ "node": ">=18" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -368,6 +2713,20 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/core-js-compat": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -382,11 +2741,36 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -411,6 +2795,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -420,12 +2810,58 @@ "node": ">=0.3.1" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.182", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", + "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -451,11 +2887,19 @@ "errno": "cli.js" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -472,12 +2916,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -501,6 +3012,40 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -558,6 +3103,26 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -623,12 +3188,18 @@ "node": ">= 6" } }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "optional": true + "dev": true }, "node_modules/has-flag": { "version": "4.0.0", @@ -639,6 +3210,19 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -648,6 +3232,44 @@ "he": "bin/he" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -676,8 +3298,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -695,7 +3315,27 @@ "image-size": "bin/image-size.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/inflight": { @@ -715,6 +3355,16 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -733,6 +3383,22 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -784,6 +3450,25 @@ "node": ">=8" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -820,6 +3505,38 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -832,6 +3549,85 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -986,6 +3782,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1001,6 +3807,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1105,6 +3918,16 @@ "node": ">=0.10.0" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -1151,6 +3974,29 @@ "node": ">=4" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -1228,26 +4074,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/needle": { "version": "3.3.1", @@ -1266,6 +4096,20 @@ "node": ">= 4.4.x" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1302,6 +4146,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1356,6 +4206,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -1365,6 +4225,18 @@ "node": ">= 0.10" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1383,6 +4255,20 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1417,6 +4303,75 @@ "node": ">=6" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", @@ -1439,6 +4394,15 @@ "dev": true, "optional": true }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1460,6 +4424,90 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -1478,6 +4526,60 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -1527,6 +4629,12 @@ "node": ">=0.10.0" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1550,16 +4658,46 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, - "optional": true + "optional": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } }, "node_modules/semver": { "version": "5.7.2", @@ -1580,6 +4718,29 @@ "randombytes": "^2.1.0" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1650,6 +4811,27 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -1715,6 +4897,130 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1727,6 +5033,30 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -1813,6 +5143,274 @@ "dev": true, "optional": true }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.100.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.0.tgz", + "integrity": "sha512-H8yBSBTk+BqxrINJnnRzaxU94SVP2bjd7WmA+PfCphoIdDpeQMJ77pq9/4I7xjLq38cB1bNKfzYPZu8pB3zKtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.2", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1828,6 +5426,13 @@ "node": ">= 8" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -1925,6 +5530,42 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -1934,6 +5575,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", diff --git a/package.json b/package.json index 32f29eab2..f4262b05a 100644 --- a/package.json +++ b/package.json @@ -13,16 +13,26 @@ "quickstart.html" ], "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/preset-env": "^7.28.0", "@types/mocha": ">=5.2.7", + "babel-loader": "^10.0.0", "husky": ">=9.0", "less": ">=4.0", "lint-staged": ">=12.3.1", "mocha": ">=2.4.1", "prettier": "~3.2.0", "typescript": "~5.4", - "uglify-js": "2.x" + "uglify-js": "2.x", + "webpack": "^5.100.0", + "webpack-cli": "^6.0.1" }, "lint-staged": { "*.{ts,js,css,html}": "prettier --write" + }, + "dependencies": { + "@fluent/bundle": "^0.19.1", + "@fluent/syntax": "^0.19.0", + "jsdom": "^26.1.0" } } diff --git a/script/bundle-fluent.js b/script/bundle-fluent.js new file mode 100755 index 000000000..e485cffa4 --- /dev/null +++ b/script/bundle-fluent.js @@ -0,0 +1,69 @@ +#!/usr/bin/env node + +/** + * Script to bundle Fluent libraries for MathQuill + * Run this script if you need to update the Fluent bundle + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +console.log('📦 Bundling Fluent libraries for MathQuill...'); + +// Create temporary entry file +const entryFile = ` +const { FluentBundle, FluentResource } = require('@fluent/bundle'); +const { parse } = require('@fluent/syntax'); +module.exports = { FluentBundle, FluentResource, parseResource: parse }; +`; + +fs.writeFileSync('temp-fluent-entry.js', entryFile); + +// Create temporary webpack config +const webpackConfig = ` +const path = require('path'); +module.exports = { + mode: 'production', + entry: './temp-fluent-entry.js', + output: { + path: path.resolve(__dirname, 'src/services'), + filename: 'fluent-bundle.js', + library: { + name: 'FluentLib', + type: 'var' + } + }, + target: 'web' +}; +`; + +fs.writeFileSync('temp-webpack.config.js', webpackConfig); + +try { + // Run webpack + execSync('npx webpack --config temp-webpack.config.js', { stdio: 'inherit' }); + + // Append global exports for legacy compatibility + const globalExports = ` +var FluentBundle = FluentLib.FluentBundle; +var FluentResource = FluentLib.FluentResource; +var parseResource = FluentLib.parseResource; +`; + + fs.appendFileSync('src/services/fluent-bundle.js', globalExports); + + console.log('✅ Fluent bundle generated successfully'); + console.log('📍 Location: src/services/fluent-bundle.js'); +} catch (error) { + console.error('❌ Webpack bundling failed:', error.message); + process.exit(1); +} finally { + // Clean up temporary files + if (fs.existsSync('temp-fluent-entry.js')) { + fs.unlinkSync('temp-fluent-entry.js'); + } + if (fs.existsSync('temp-webpack.config.js')) { + fs.unlinkSync('temp-webpack.config.js'); + } +} diff --git a/script/generate-locale-imports.js b/script/generate-locale-imports.js new file mode 100644 index 000000000..965f6ec80 --- /dev/null +++ b/script/generate-locale-imports.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node + +/** + * Script to generate locale-imports.ts from .ftl files + * This reads the Fluent message files and creates the embedded locale imports + */ + +const fs = require('fs'); +const path = require('path'); + +console.log('📦 Generating locale imports from .ftl files...'); + +const localeDir = path.join(__dirname, '..', 'src', 'locale'); +const outputFile = path.join( + __dirname, + '..', + 'src', + 'services', + 'locale-imports.ts' +); + +// Find all language directories +const languages = fs + .readdirSync(localeDir, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + +console.log(`Found languages: ${languages.join(', ')}`); + +// Read messages for each language +const embeddedMessages = {}; +for (const lang of languages) { + const messagesFile = path.join(localeDir, lang, 'messages.ftl'); + if (fs.existsSync(messagesFile)) { + const content = fs.readFileSync(messagesFile, 'utf8'); + // Strip out Fluent comments (lines starting with #) and empty lines + const strippedContent = content + .split('\n') + .filter((line) => !line.trim().startsWith('#') && line.trim() !== '') + .join('\n') + .trim(); // Ensure clean content without trailing whitespace + embeddedMessages[lang] = strippedContent; + console.log( + `✓ Loaded ${lang} messages (${content.length} chars -> ${strippedContent.length} chars after stripping comments)` + ); + } else { + console.warn(`⚠ Missing messages.ftl for language: ${lang}`); + } +} + +// Generate the TypeScript file +const template = `/** + * MathQuill Locale Messages and Import Functions + * + * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY + * Generated from .ftl files in src/locale/ by script/generate-locale-imports.js + * + * This file contains embedded Fluent localization messages for MathQuill's mathspeak functionality. + * It provides synchronous access to locale messages without requiring external file loading. + * + * Currently supports: ${languages.join(', ')} + * + * To add a new language: + * 1. Create src/locale/[lang]/messages.ftl with Fluent message strings + * 2. Run 'node script/generate-locale-imports.js' to regenerate this file + * 3. Add the language code to AVAILABLE_LOCALES if needed + */ + +// Embedded locale messages for immediate synchronous access +const EMBEDDED_MESSAGES: Record = { +${Object.entries(embeddedMessages) + .map( + ([lang, content]) => + ` ${lang}: \`${content.replace(/`/g, '\\`').replace(/\$/g, '\\$')}\`` + ) + .join(',\n\n')} +}; + +function loadLocaleMessages(language: string): string | null { + // Resolve the language to the actual language that will be loaded + const resolvedLanguage = getResolvedLanguage(language); + + // Get the embedded messages + const messages = EMBEDDED_MESSAGES[resolvedLanguage]; + if (messages) { + if (resolvedLanguage !== language) { + console.info(\`Language '\${language}' resolved to '\${resolvedLanguage}'\`); + } + return messages; + } + + console.warn(\`No messages found for language: \${resolvedLanguage}\`); + return null; +} + +/** + * Finds the best fallback language for a given language code. + * Supports locale-specific codes (e.g., en-US → en, es-MX → es) + */ +function findFallbackLanguage(language: string): string | null { + const normalizedLanguage = language.toLowerCase(); + + // If it's a locale-specific code (e.g., en-GB), try the base language + if (normalizedLanguage.includes('-')) { + const baseLanguage = normalizedLanguage.split('-')[0]; + if (AVAILABLE_LOCALES.includes(baseLanguage as SupportedLocale)) { + return baseLanguage; + } + } + + return null; +} + +// Available locales registry +const AVAILABLE_LOCALES = ${JSON.stringify(languages)} as const; + +type SupportedLocale = (typeof AVAILABLE_LOCALES)[number]; + +function isSupportedLocale(locale: string): locale is SupportedLocale { + return AVAILABLE_LOCALES.includes(locale as SupportedLocale); +} + +/** + * Checks if a language is supported, either directly or through fallback. + */ +function hasLanguageSupport(language: string): boolean { + const normalizedLanguage = language.toLowerCase(); + + // Check exact match first + if (isSupportedLocale(normalizedLanguage)) { + return true; + } + + // Check if fallback is available + const fallback = findFallbackLanguage(normalizedLanguage); + return fallback !== null; +} + +/** + * Gets the actual language that will be loaded for a given language request. + */ +function getResolvedLanguage(language: string): string { + const normalizedLanguage = language.toLowerCase(); + + // Check exact match first + if (isSupportedLocale(normalizedLanguage)) { + return normalizedLanguage; + } + + // Try fallback + const fallback = findFallbackLanguage(normalizedLanguage); + if (fallback && isSupportedLocale(fallback)) { + return fallback; + } + + // Final fallback to English + return 'en'; +} + +// Functions are made globally available below for MathQuill's concatenation build system + +// @ts-ignore - Make functions globally available +if (typeof window !== 'undefined') { + (window as any).loadLocaleMessages = loadLocaleMessages; + (window as any).getResolvedLanguage = getResolvedLanguage; + (window as any).hasLanguageSupport = hasLanguageSupport; +} else { + (global as any).loadLocaleMessages = loadLocaleMessages; + (global as any).getResolvedLanguage = getResolvedLanguage; + (global as any).hasLanguageSupport = hasLanguageSupport; +} +`; + +// Write the generated file +fs.writeFileSync(outputFile, template, 'utf8'); + +console.log('✅ Locale imports generated successfully!'); +console.log(`📍 Location: ${outputFile}`); +console.log( + `📊 Languages: ${languages.length}, Total messages: ${Object.values(embeddedMessages).reduce((sum, content) => sum + content.length, 0)} chars` +); +console.log('🏁 Locale import generation complete!'); diff --git a/src/animate.ts b/src/animate.ts index e830542de..cc4d700fb 100644 --- a/src/animate.ts +++ b/src/animate.ts @@ -25,7 +25,7 @@ */ const animate = (function () { // IIFE exists just to hang on to configured rafShim and cancelShim - // functions + // functions. Both return/accept number tokens for cross-environment compatibility. let rafShim: (cb: () => void) => number, cancelShim: (token: number) => void; if ( typeof requestAnimationFrame === 'function' && @@ -34,8 +34,10 @@ const animate = (function () { rafShim = requestAnimationFrame; cancelShim = cancelAnimationFrame; } else { - rafShim = (cb: () => void) => setTimeout(cb, 13); - cancelShim = clearTimeout; + // setTimeout fallback with type coercion for consistent number-based token handling + rafShim = (cb: () => void) => setTimeout(cb, 13) as unknown as number; + cancelShim = (token: number) => + clearTimeout(token as unknown as NodeJS.Timeout); } return function ( diff --git a/src/commands/math.ts b/src/commands/math.ts index f86dbdf98..c46a61320 100644 --- a/src/commands/math.ts +++ b/src/commands/math.ts @@ -520,6 +520,7 @@ class MathBlock extends MathElement { var tempOp = ''; var autoOps: CursorOptions['autoOperatorNames'] = {}; if (this.controller) autoOps = this.controller.options.autoOperatorNames; + const outerThis = this; // Capture outer 'this' for use in callback return ( this.foldChildren([], function (speechArray, cmd) { if (cmd.isPartOfOperator) { @@ -528,7 +529,15 @@ class MathBlock extends MathElement { if (tempOp !== '') { if (autoOps._maxLength! > 0) { var x = autoOps[tempOp.toLowerCase()]; - if (typeof x === 'string') tempOp = x; + if (typeof x === 'string') { + tempOp = x; + // Check if there's a localized version of this auto operator + const localization = getControllerLocalization(outerThis); + const localizedOp = localization.formatAutoOperator(tempOp); + if (localizedOp !== tempOp) { + tempOp = localizedOp; + } + } } speechArray.push(tempOp + ' '); tempOp = ''; @@ -643,7 +652,9 @@ class MathBlock extends MathElement { cmd.createLeftOf(cursor.show()); // special-case the slash so that fractions are voiced while typing if (ch === '/') { - cursor.controller.aria.alert('over'); + cursor.controller.aria.alert( + getControllerLocalization(cursor.parent).formatMessage('over') + ); } else { cursor.controller.aria.alert(cmd.mathspeak({ createdLeftOf: cursor })); } @@ -707,7 +718,9 @@ API.StaticMath = function (APIClasses: APIClasses) { static RootBlock = MathBlock; __mathquillify(opts: ConfigOptions, _interfaceVersion: number) { - this.config(opts as MathQuill.v3.Config); + // Internal config method takes additional isInitialConfig parameter + // Cast necessary because public API only exposes single-parameter version + (this.config as any)(opts, true); // `mathquillify` calls `createTextarea` super.mathquillify('mq-math-mode'); this.__controller.setupStaticField(); @@ -757,7 +770,9 @@ API.MathField = function (APIClasses: APIClasses) { static RootBlock = RootMathBlock; __mathquillify(opts: ConfigOptions, interfaceVersion: number) { - this.config(opts as MathQuill.v3.Config); + // Internal config method takes additional isInitialConfig parameter + // Cast necessary because public API only exposes single-parameter version + (this.config as any)(opts, true); if (interfaceVersion > 1) this.__controller.root.reflow = noop; super.mathquillify('mq-editable-field mq-math-mode'); // TODO: Why does this need to be deleted (contrary to the type definition)? Could we set it to `noop` instead? diff --git a/src/commands/math/advancedSymbols.ts b/src/commands/math/advancedSymbols.ts index fec6ae569..6cb2742ef 100644 --- a/src/commands/math/advancedSymbols.ts +++ b/src/commands/math/advancedSymbols.ts @@ -617,18 +617,43 @@ LatexCmds['⌉'] = LatexCmds.rceil = bindVanillaSymbol( '⌉', 'right ceiling' ); -LatexCmds.opencurlybrace = LatexCmds.lbrace = bindVanillaSymbol( - '\\lbrace ', - '{', - 'left brace' -); -LatexCmds.closecurlybrace = LatexCmds.rbrace = bindVanillaSymbol( - '\\rbrace ', - '}', - 'right brace' -); -LatexCmds.lbrack = bindVanillaSymbol('[', 'left bracket'); -LatexCmds.rbrack = bindVanillaSymbol(']', 'right bracket'); +class LeftBrace extends VanillaSymbol { + constructor() { + super('\\lbrace ', h.text('{')); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('left-brace'); + } +} +class RightBrace extends VanillaSymbol { + constructor() { + super('\\rbrace ', h.text('}')); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('right-brace'); + } +} +class LeftBracket extends VanillaSymbol { + constructor() { + super('[', h.text('[')); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('left-bracket'); + } +} +class RightBracket extends VanillaSymbol { + constructor() { + super(']', h.text(']')); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('right-bracket'); + } +} + +LatexCmds.opencurlybrace = LatexCmds.lbrace = () => new LeftBrace(); +LatexCmds.closecurlybrace = LatexCmds.rbrace = () => new RightBrace(); +LatexCmds.lbrack = () => new LeftBracket(); +LatexCmds.rbrack = () => new RightBracket(); //various symbols LatexCmds.slash = bindVanillaSymbol('/', 'slash'); diff --git a/src/commands/math/basicSymbols.ts b/src/commands/math/basicSymbols.ts index a59369a02..c89eb5f30 100644 --- a/src/commands/math/basicSymbols.ts +++ b/src/commands/math/basicSymbols.ts @@ -760,6 +760,25 @@ baseOptionProcessors.disableAutoSubstitutionInSubscripts = function ( return { except: splitWordsIntoDict((opt as any).except) }; }; +Options.prototype.language = 'en'; +baseOptionProcessors.language = function (lang: unknown) { + if (typeof lang !== 'string') { + throw '"' + lang + '" not a valid language code'; + } + // Validate language code (basic check for 2-letter ISO codes) + if (!/^[a-z]{2}(-[a-z]{2})?$/i.test(lang)) { + throw ( + '"' + + lang + + '" not a valid language code (expected format: "en" or "en-US")' + ); + } + + // The language will be resolved to the best available match during loading + // This validation just ensures the format is correct + return lang; +}; + function splitWordsIntoDict(cmds: unknown) { if (typeof cmds !== 'string') { throw '"' + cmds + '" not a space-delimited list'; @@ -858,12 +877,20 @@ LatexCmds.f = class extends Letter { LatexCmds[' '] = LatexCmds.space = () => new DigitGroupingChar('\\ ', h('span', {}, [h.text(U_NO_BREAK_SPACE)]), ' '); -LatexCmds['.'] = () => - new DigitGroupingChar( - '.', - h('span', { class: 'mq-digit' }, [h.text('.')]), - '.' - ); +class DotSymbol extends DigitGroupingChar { + constructor() { + super('.', h('span', { class: 'mq-digit' }, [h.text('.')]), '.'); + } + mathspeak(opts?: MathspeakOptions): string { + // If this dot is being created by the user (typing or navigating to it explicitly), + // use the localized word. Otherwise, return the actual character so it's pronounced naturally as part of a number. + if (opts && opts.createdLeftOf) { + return getControllerLocalization(this).formatMessage('dot'); + } + return '.'; + } +} +LatexCmds['.'] = () => new DotSymbol(); LatexCmds["'"] = LatexCmds.prime = bindVanillaSymbol("'", '′', 'prime'); LatexCmds['″'] = LatexCmds.dprime = bindVanillaSymbol( @@ -1265,7 +1292,10 @@ LatexCmds['+'] = class extends PlusMinus { super('+', h.text('+')); } mathspeak(): string { - return plusMinusIsBinaryOperator(this) ? 'plus' : 'positive'; + const localization = getControllerLocalization(this); + return plusMinusIsBinaryOperator(this) + ? localization.formatMessage('plus') + : localization.formatMessage('positive'); } }; @@ -1275,7 +1305,10 @@ class MinusNode extends PlusMinus { super('-', h.entityText('−')); } mathspeak(): string { - return plusMinusIsBinaryOperator(this) ? 'minus' : 'negative'; + const localization = getControllerLocalization(this); + return plusMinusIsBinaryOperator(this) + ? localization.formatMessage('minus') + : localization.formatMessage('negative'); } } LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = MinusNode; @@ -1290,10 +1323,15 @@ LatexCmds.mp = LatexCmds.minusplus = () => new PlusMinus('\\mp ', h.entityText('∓'), 'minus-or-plus'); -CharCmds['*'] = - LatexCmds.sdot = - LatexCmds.cdot = - bindBinaryOperator('\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better +class TimesOperator extends BinaryOperator { + constructor() { + super('\\cdot ', h.entityText('·'), '*'); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('times'); + } +} +CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = () => new TimesOperator(); //semantically should be ⋅, but · looks better class To extends BinaryOperator { constructor() { @@ -1344,6 +1382,18 @@ class Inequality extends BinaryOperator { this.textTemplate = [this.data[`text${strictness}`]]; this.mathspeakName = this.data[`mathspeak${strictness}`]; } + mathspeak(): string { + const localization = getControllerLocalization(this); + const isLess = this.data === less; + + if (this.strict) { + return localization.formatMessage(isLess ? 'less-than' : 'greater-than'); + } else { + return localization.formatMessage( + isLess ? 'less-than-or-equal-to' : 'greater-than-or-equal-to' + ); + } + } deleteTowards(dir: Direction, cursor: Cursor) { if (dir === L && !this.strict) { this.swap(true); @@ -1414,15 +1464,24 @@ LatexCmds['∞'] = LatexCmds.infin = LatexCmds.infinity = bindVanillaSymbol('\\infty ', '∞', 'infinity'); -LatexCmds['≠'] = - LatexCmds.ne = - LatexCmds.neq = - bindBinaryOperator('\\ne ', '≠', 'not equal'); +class NotEqual extends BinaryOperator { + constructor() { + super('\\ne ', h.entityText('≠'), '≠', 'not equal'); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('not-equal-to'); + } +} + +LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = NotEqual; class Equality extends BinaryOperator { constructor() { super('=', h.text('='), '=', 'equals'); } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('equals'); + } createLeftOf(cursor: Cursor) { var cursorL = cursor[L]; if (cursorL instanceof Inequality && cursorL.strict) { @@ -1473,6 +1532,11 @@ class Approx extends BinaryOperator { constructor() { super('\\approx ', h.entityText('≈'), '≈', 'approximately equal'); } + mathspeak(): string { + return getControllerLocalization(this).formatMessage( + 'approximately-equal-to' + ); + } deleteTowards(dir: Direction, cursor: Cursor) { if (dir === L) { var l = cursor[L] as MQNode; @@ -1566,3 +1630,26 @@ LatexCmds['≁'] = LatexCmds.nsim = bindBinaryOperator( 'nsim', 'not similar' ); + +// Localized parenthesis symbols +class LeftParenthesis extends VanillaSymbol { + constructor() { + super('(', h.text('(')); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('left-parenthesis'); + } +} +class RightParenthesis extends VanillaSymbol { + constructor() { + super(')', h.text(')')); + } + mathspeak(): string { + return getControllerLocalization(this).formatMessage('right-parenthesis'); + } +} + +// These might not be needed as parentheses are usually handled by the Bracket system, +// but adding them for completeness in case individual parentheses are used +LatexCmds.lparen = () => new LeftParenthesis(); +LatexCmds.rparen = () => new RightParenthesis(); diff --git a/src/commands/math/commands.ts b/src/commands/math/commands.ts index 9a36aa642..b34586e26 100644 --- a/src/commands/math/commands.ts +++ b/src/commands/math/commands.ts @@ -1,6 +1,7 @@ /*************************** * Commands and Operators. **************************/ + var SVG_SYMBOLS = { sqrt: { width: '', @@ -118,16 +119,47 @@ class Style extends MathCommand { ); this.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); - this.mathspeakTemplate = [ - 'Start' + this.ariaLabel + ',', - 'End' + this.ariaLabel - ]; // In most cases, mathspeak should announce the start and end of style blocks. // There is one exception currently (mathrm). this.shouldNotSpeakDelimiters = opts && opts.shouldNotSpeakDelimiters; + + // Style blocks will have their mathspeak templates set by updateMathspeak when needed } mathspeak(opts?: MathspeakOptions) { if (!this.shouldNotSpeakDelimiters || (opts && opts.ignoreShorthand)) { + if ( + !this.mathspeakTemplate || + this.mathspeakTemplate.length === 0 || + this.mathspeakTemplate[0] === '' + ) { + const localization = getControllerLocalization(this); + if (this.ariaLabel) { + const ariaLabel = this.ariaLabel.toLowerCase().replace(/\s+/g, '-'); + const startKey = `start-${ariaLabel}`; + const endKey = `end-${ariaLabel}`; + + if ( + localization.hasMessage(startKey) && + localization.hasMessage(endKey) + ) { + this.mathspeakTemplate = localization.createMathspeakTemplate( + startKey, + endKey + ); + } else { + this.mathspeakTemplate = [ + localization.formatMessage('generic-start') + + ' ' + + this.ariaLabel + + ',', + ', ' + + localization.formatMessage('generic-end') + + ' ' + + this.ariaLabel + ]; + } + } + } return super.mathspeak(); } return this.foldChildren('', function (speech, block) { @@ -251,10 +283,7 @@ LatexCmds.textcolor = class extends MathCommand { ) ); this.ariaLabel = color.replace(/^\\/, ''); - this.mathspeakTemplate = [ - 'Start ' + this.ariaLabel + ',', - 'End ' + this.ariaLabel - ]; + // Mathspeak template will be set by updateMathspeak when needed } latexRecursive(ctx: LatexContext) { this.checkCursorContextOpen(ctx); @@ -278,6 +307,29 @@ LatexCmds.textcolor = class extends MathCommand { return super.parser(); }); } + mathspeak() { + if ( + !this.mathspeakTemplate || + this.mathspeakTemplate.length === 0 || + this.mathspeakTemplate[0] === '' + ) { + const localization = getControllerLocalization(this); + if (this.ariaLabel) { + // For textcolor, use the color name directly as start/end delimiters + this.mathspeakTemplate = [ + localization.formatMessage('generic-start') + + ' ' + + this.ariaLabel + + ',', + ', ' + + localization.formatMessage('generic-end') + + ' ' + + this.ariaLabel + ]; + } + } + return super.mathspeak(); + } isStyleBlock() { return true; } @@ -303,10 +355,7 @@ var Class = (LatexCmds['class'] = class extends MathCommand { h.block('span', { class: `mq-class ${cls}` }, blocks[0]) ); this.ariaLabel = cls + ' class'; - this.mathspeakTemplate = [ - 'Start ' + this.ariaLabel + ',', - 'End ' + this.ariaLabel - ]; + // Mathspeak template will be set by updateMathspeak when needed return super.parser(); }); } @@ -320,6 +369,29 @@ var Class = (LatexCmds['class'] = class extends MathCommand { this.checkCursorContextClose(ctx); } + mathspeak() { + if ( + !this.mathspeakTemplate || + this.mathspeakTemplate.length === 0 || + this.mathspeakTemplate[0] === '' + ) { + const localization = getControllerLocalization(this); + if (this.ariaLabel) { + // For class, use the class name directly as start/end delimiters + this.mathspeakTemplate = [ + localization.formatMessage('generic-start') + + ' ' + + this.ariaLabel + + ',', + ', ' + + localization.formatMessage('generic-end') + + ' ' + + this.ariaLabel + ]; + } + } + return super.mathspeak(); + } isStyleBlock() { return true; } @@ -626,11 +698,17 @@ class SubscriptCommand extends SupSub { ); textTemplate = ['_']; - - mathspeakTemplate = ['Subscript,', ', Baseline']; - ariaLabel = 'subscript'; + mathspeak() { + const localization = getControllerLocalization(this); + this.mathspeakTemplate = [ + localization.formatMessage('subscript') + ',', + ', ' + localization.formatMessage('baseline') + ]; + return super.mathspeak(); + } + finalizeTree() { this.downInto = this.sub = this.getEnd(L); this.sub.upOutOf = insLeftOfMeUnlessAtEnd; @@ -652,7 +730,15 @@ LatexCmds.superscript = ); textTemplate = ['^(', ')']; + ariaLabel = 'superscript'; + mathspeak(opts?: MathspeakOptions) { + const localization = getControllerLocalization(this); + this.mathspeakTemplate = [ + localization.formatMessage('superscript') + ',', + ', ' + localization.formatMessage('baseline') + ]; + // Simplify basic exponent speech for common whole numbers. var child = this.upInto; if (child !== undefined) { @@ -662,39 +748,24 @@ LatexCmds.superscript = var innerText = getCtrlSeqsFromBlock(child); // If the superscript is a whole number, shorten the speech that is returned. if ((!opts || !opts.ignoreShorthand) && intRgx.test(innerText)) { + var powerNumber = parseInt(innerText); + // Simple cases - if (innerText === '0') { - return 'to the 0 power'; - } else if (innerText === '2') { - return 'squared'; - } else if (innerText === '3') { - return 'cubed'; + if (powerNumber === 0) { + return localization.formatMessage('power-zero'); + } else if (powerNumber === 2) { + return localization.formatMessage('power-squared'); + } else if (powerNumber === 3) { + return localization.formatMessage('power-cubed'); } - // More complex cases. - var suffix = ''; - // Limit suffix addition to exponents < 1000. - if (/^[+-]?\d{1,3}$/.test(innerText)) { - if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { - suffix = 'th'; - } else if (/1$/.test(innerText)) { - suffix = 'st'; - } else if (/2$/.test(innerText)) { - suffix = 'nd'; - } else if (/3$/.test(innerText)) { - suffix = 'rd'; - } - } - var innerMathspeak = - typeof child === 'object' ? child.mathspeak() : innerText; - return 'to the ' + innerMathspeak + suffix + ' power'; + // More complex cases - use localized ordinal power format + return localization.formatPowerExpression(powerNumber); } } return super.mathspeak(); } - ariaLabel = 'superscript'; - mathspeakTemplate = ['Superscript,', ', Baseline']; finalizeTree() { this.upInto = this.sup = this.getEnd(R); this.sup.downOutOf = insLeftOfMeUnlessAtEnd; @@ -703,6 +774,8 @@ LatexCmds.superscript = }; class SummationNotation extends MathCommand { + ariaLabel: string; + constructor(ch: string, symbol: string, ariaLabel?: string) { super(); @@ -749,16 +822,19 @@ class SummationNotation extends MathCommand { this.checkCursorContextClose(ctx); } mathspeak() { + const localization = getControllerLocalization(this); + const ariaLabelLower = this.ariaLabel.toLowerCase().replace(/ /g, '-'); + return ( - 'Start ' + - this.ariaLabel + - ' from ' + + localization.formatMessage(`start-${ariaLabelLower}`) + + ' ' + this.getEnd(L).mathspeak() + - ' to ' + + ', ' + + localization.formatMessage('to') + + ', ' + this.getEnd(R).mathspeak() + - ', end ' + - this.ariaLabel + - ', ' + ', ' + + localization.formatMessage(`end-${ariaLabelLower}`) ); } parser() { @@ -864,14 +940,19 @@ var Fraction = this.downInto = endsL.downOutOf = endsR; endsL.ariaLabel = 'numerator'; endsR.ariaLabel = 'denominator'; + const localization = getControllerLocalization(this); if (this.getFracDepth() > 1) { this.mathspeakTemplate = [ - 'StartNestedFraction,', - 'NestedOver', - ', EndNestedFraction' + localization.formatMessage('start-nested-fraction') + ',', + ' ' + localization.formatMessage('nested-over') + ' ', + ', ' + localization.formatMessage('end-nested-fraction') ]; } else { - this.mathspeakTemplate = ['StartFraction,', 'Over', ', EndFraction']; + this.mathspeakTemplate = [ + localization.formatMessage('start-fraction') + ',', + ' ' + localization.formatMessage('over') + ' ', + ', ' + localization.formatMessage('end-fraction') + ]; } } @@ -890,24 +971,40 @@ var Fraction = intRgx.test(numText) && intRgx.test(denText) ) { - var isSingular = numText === '1' || numText === '-1'; + var numerator = parseInt(numText); + var denominator = parseInt(denText); + const localization = getControllerLocalization(this); var newDenSpeech = ''; - if (denText === '2') { - newDenSpeech = isSingular ? 'half' : 'halves'; - } else if (denText === '3') { - newDenSpeech = isSingular ? 'third' : 'thirds'; - } else if (denText === '4') { - newDenSpeech = isSingular ? 'quarter' : 'quarters'; - } else if (denText === '5') { - newDenSpeech = isSingular ? 'fifth' : 'fifths'; - } else if (denText === '6') { - newDenSpeech = isSingular ? 'sixth' : 'sixths'; - } else if (denText === '7') { - newDenSpeech = isSingular ? 'seventh' : 'sevenths'; - } else if (denText === '8') { - newDenSpeech = isSingular ? 'eighth' : 'eighths'; - } else if (denText === '9') { - newDenSpeech = isSingular ? 'ninth' : 'ninths'; + + // Use localized fraction shortcuts for denominators 2-9 + if (denominator >= 2 && denominator <= 9) { + // Try the new denominator-only format first + const denominatorPart = localization.formatFractionDenominator( + numerator, + denominator + ); + + if (denominatorPart !== '') { + // Handle negative numerators properly + let numeratorText: string; + if (numerator < 0) { + // For negative numerators, use localized "negative" + absolute value + numeratorText = + localization.formatMessage('negative') + + ' ' + + Math.abs(numerator); + } else { + numeratorText = numerator.toString(); + } + // Combine numerator with properly pluralized denominator + newDenSpeech = numeratorText + ' ' + denominatorPart; + } else { + // Fall back to the old full format + newDenSpeech = localization.formatFractionShortcut( + numerator, + denominator + ); + } } if (newDenSpeech !== '') { var output = ''; @@ -931,9 +1028,10 @@ var Fraction = } } if (precededByInteger) { - output += 'and '; + const localization = getControllerLocalization(this); + output += localization.formatMessage('and') + ' '; } - output += this.getEnd(L).mathspeak() + ' ' + newDenSpeech; + output += newDenSpeech; return output; } } @@ -1053,7 +1151,7 @@ class Token extends MQSymbol { tokenId = ''; ctrlSeq = '\\token'; textTemplate = ['token(', ')']; - mathspeakTemplate = ['StartToken,', ', EndToken']; + mathspeakTemplate = ['token(', ')']; // Static fallback, not used by mathspeak method ariaLabel = 'token'; html(): Element | DocumentFragment { @@ -1118,8 +1216,16 @@ class SquareRoot extends MathCommand { ]) ); textTemplate = ['sqrt(', ')']; - mathspeakTemplate = ['StartRoot,', ', EndRoot']; ariaLabel = 'root'; + + mathspeak() { + const localization = getControllerLocalization(this); + this.mathspeakTemplate = localization.createMathspeakTemplate( + 'start-root', + 'end-root' + ); + return super.mathspeak(); + } parser() { return latexMathParser.optBlock .then(function (optBlock) { @@ -1188,16 +1294,29 @@ class NthRoot extends SquareRoot { var radicandMathspeak = this.getEnd(R).mathspeak(); this.getEnd(L).ariaLabel = 'Index'; this.getEnd(R).ariaLabel = 'Radicand'; + + const localization = getControllerLocalization(this); + if (indexMathspeak === '3') { // cube root - return 'Start Cube Root, ' + radicandMathspeak + ', End Cube Root'; + return ( + localization.formatMessage('start-cube-root') + + ', ' + + radicandMathspeak + + ', ' + + localization.formatMessage('end-cube-root') + ); } else { return ( - 'Root Index ' + + localization.formatMessage('root-index') + + ' ' + indexMathspeak + - ', Start Root, ' + + ', ' + + localization.formatMessage('start-nth-root') + + ', ' + radicandMathspeak + - ', End Root' + ', ' + + localization.formatMessage('end-nth-root') ); } } @@ -1338,20 +1457,35 @@ class Bracket extends DelimsNode { var open = this.sides[L].ch, close = this.sides[R].ch; if (open === '|' && close === '|') { - this.mathspeakTemplate = ['StartAbsoluteValue,', ', EndAbsoluteValue']; + const localization = getControllerLocalization(this); + this.mathspeakTemplate = localization.createMathspeakTemplate( + 'start-absolute-value', + 'end-absolute-value' + ); this.ariaLabel = 'absolute value'; } else if (opts && opts.createdLeftOf && this.side) { var ch = ''; if (this.side === L) ch = this.textTemplate[0]; else if (this.side === R) ch = this.textTemplate[1]; + const localization = getControllerLocalization(this); return ( - (this.side === L ? 'left ' : 'right ') + + (this.side === L + ? localization.formatMessage('bracket-left') + : localization.formatMessage('bracket-right')) + + ' ' + BRACKET_NAMES[ch as keyof typeof BRACKET_NAMES] ); } else { + const localization = getControllerLocalization(this); this.mathspeakTemplate = [ - 'left ' + BRACKET_NAMES[open as keyof typeof BRACKET_NAMES] + ',', - ', right ' + BRACKET_NAMES[close as keyof typeof BRACKET_NAMES] + localization.formatMessage('bracket-left') + + ' ' + + BRACKET_NAMES[open as keyof typeof BRACKET_NAMES] + + ',', + ', ' + + localization.formatMessage('bracket-right') + + ' ' + + BRACKET_NAMES[close as keyof typeof BRACKET_NAMES] ]; this.ariaLabel = BRACKET_NAMES[open as keyof typeof BRACKET_NAMES] + ' block'; @@ -1728,7 +1862,6 @@ class Binomial extends DelimsNode { ); textTemplate = ['choose(', ',', ')']; - mathspeakTemplate = ['StartBinomial,', 'Choose', ', EndBinomial']; ariaLabel = 'binomial'; finalizeTree() { @@ -1739,6 +1872,13 @@ class Binomial extends DelimsNode { // https://math.stackexchange.com/a/1617456 cites Knuth as the source of 'upper index' and 'lower index' endsL.ariaLabel = 'upper index'; endsR.ariaLabel = 'lower index'; + + const localization = getControllerLocalization(this); + this.mathspeakTemplate = localization.createMathspeakTemplate( + 'start-binomial', + 'choose', + 'end-binomial' + ); } } diff --git a/src/commands/text.ts b/src/commands/text.ts index 9db40828b..ff58efce9 100644 --- a/src/commands/text.ts +++ b/src/commands/text.ts @@ -103,15 +103,23 @@ class TextBlock extends MQNode { return out; } - mathspeakTemplate = ['StartText', 'EndText']; + finalizeTree() { + const localization = getControllerLocalization(this); + this.mathspeakTemplate = localization.createMathspeakTemplate( + 'start-text', + 'end-text' + ); + super.finalizeTree({} as CursorOptions); + } + mathspeak(opts?: MathspeakOptions) { if (opts && opts.ignoreShorthand) { return ( - this.mathspeakTemplate[0] + + this.mathspeakTemplate![0] + ', ' + this.textContents() + ', ' + - this.mathspeakTemplate[1] + this.mathspeakTemplate![1] ); } else { return this.textContents(); @@ -435,9 +443,17 @@ function makeTextBlock( ) { return class extends TextBlock { ctrlSeq = latex; - mathspeakTemplate = ['Start' + ariaLabel, 'End' + ariaLabel]; ariaLabel = ariaLabel; + mathspeak() { + const localization = getControllerLocalization(this); + this.mathspeakTemplate = [ + localization.formatMessage('generic-start') + ' ' + ariaLabel + ',', + ', ' + localization.formatMessage('generic-end') + ' ' + ariaLabel + ]; + return super.mathspeak(); + } + html() { const out = h(tagName, attrs, [h.text(this.textContents())]); this.setDOM(out); diff --git a/src/controller.ts b/src/controller.ts index 9818361d2..b7b8fe896 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -22,9 +22,12 @@ class ControllerBase { aria: Aria; ariaLabel: string; ariaPostLabel: string; + language: string; // Store this controller's language + private explicitLanguage: boolean; // Track if language was explicitly set + private localization: MathQuillLocalization; // This controller's localization instance readonly cursor: Cursor; editable: boolean | undefined; - _ariaAlertTimeout: number; + _ariaAlertTimeout: TimeoutId; KIND_OF_MQ: KIND_OF_MQ; isMouseSelecting: boolean = false; @@ -36,6 +39,7 @@ class ControllerBase { textareaSpan: HTMLElement | undefined; mathspeakSpan: HTMLElement | undefined; mathspeakId: string | undefined; + unregisterLanguageChange: (() => void) | undefined; constructor( root: ControllerRoot, @@ -50,7 +54,15 @@ class ControllerBase { this.options = options; this.aria = new Aria(this.getControllerSelf()); - this.ariaLabel = 'Math Input'; + // Initialize language from options, defaulting to global language + // Start with explicitLanguage = false, will be set to true if language is explicitly configured + this.explicitLanguage = false; + this.language = options.language || getCurrentGlobalLanguage(); + // Create this controller's localization instance + this.localization = new MathQuillLocalization(this.language); + // Set the initial ARIA label based on this controller's language using Fluent + // We'll set this after the method is defined, for now use a placeholder + this.ariaLabel = ''; this.ariaPostLabel = ''; root.controller = this.getControllerSelf(); @@ -61,6 +73,32 @@ class ControllerBase { this.getControllerSelf() ); // TODO: stop depending on root.cursor, and rm it + + // Register for language change notifications to update mathspeak and aria label + this.unregisterLanguageChange = onGlobalLanguageChange(() => { + // Only update language for controllers that don't have an explicit language set + if (!this.explicitLanguage) { + // Update this controller's language to match the new global language + this.language = getCurrentGlobalLanguage(); + // Update this controller's localization instance + this.localization = new MathQuillLocalization(this.language); + + // Update the default aria label if it hasn't been customized + if ( + this.ariaLabel === + this.localization.formatMessage('default-aria-label') || + this.ariaLabel === 'Math Input' || + this.ariaLabel === 'Entrada Matemática' + ) { + this.ariaLabel = + this.localization.formatMessage('default-aria-label'); + } + this.updateMathspeak(); + } + }); + + // Now that all methods are defined, set the initial ARIA label using Fluent + this.ariaLabel = this.getDefaultAriaLabel(); } getControllerSelf() { @@ -68,6 +106,41 @@ class ControllerBase { return this as any as Controller; } + updateAriaLabel() { + // Update this controller's language to match the current global language + this.language = getCurrentGlobalLanguage(); + // Update this controller's localization instance + this.localization = new MathQuillLocalization(this.language); + // Update ARIA label if it's still the default (hasn't been customized) + const newDefaultLabel = this.getDefaultAriaLabel(); + if ( + this.ariaLabel === 'Math Input' || + this.ariaLabel === 'Entrada Matemática' + ) { + this.ariaLabel = newDefaultLabel; + } + } + + // Helper method to get the default ARIA label for this controller's language + private getDefaultAriaLabel(): string { + return this.localization.formatMessage('default-aria-label'); + } + + // Public method to get a localization instance for this controller's language + // This can be used by math elements for mathspeak generation + getLocalizationForController() { + return this.localization; + } + + // Method to update the controller's language (called when explicitly set) + setLanguage(language: string) { + this.language = language; + this.localization = new MathQuillLocalization(language); + this.explicitLanguage = true; + // Update mathspeak templates with new language + this.updateMathspeak(); + } + handle(name: HandlersWithDirection, dir: Direction): void; handle(name: HandlersWithoutDirection): void; handle( @@ -99,7 +172,7 @@ class ControllerBase { setAriaLabel(ariaLabel: string) { const oldAriaLabel = this.getAriaLabel(); if (!ariaLabel && this.editable) { - this.ariaLabel = 'Math Input'; + this.ariaLabel = this.getDefaultAriaLabel(); } else { this.ariaLabel = ariaLabel; } @@ -113,10 +186,17 @@ class ControllerBase { return this; } getAriaLabel() { - if (this.ariaLabel !== 'Math Input') { + const defaultLabel = this.getDefaultAriaLabel(); + + // Check if it's a custom label (not one of the default labels in any language) + if ( + this.ariaLabel !== defaultLabel && + this.ariaLabel !== 'Math Input' && + this.ariaLabel !== 'Entrada Matemática' + ) { return this.ariaLabel; } else if (this.editable) { - return 'Math Input'; + return defaultLabel; } else { return ''; } @@ -189,12 +269,97 @@ class ControllerBase { // based on http://www.gh-mathspeak.com/examples/quick-tutorial/ // and http://www.gh-mathspeak.com/examples/grammar-rules/ exportMathSpeak() { - return this.root.mathspeak(); + return this.root.mathspeak().trim(); } // overridden - updateMathspeak(_opts?: { emptyContent: boolean }) {} + updateMathspeak(_opts?: { emptyContent: boolean }) { + // Most templates are now dynamic and will update automatically when mathspeak() is called + // Only update the remaining static templates here + this.root.postOrder((node: MQNode) => { + const localization = this.localization; + if (node.ariaLabel === 'binomial') { + node.mathspeakTemplate = localization.createMathspeakTemplate( + 'start-binomial', + 'choose', + 'end-binomial' + ); + } else if ('isTextBlock' in node && node.isTextBlock?.()) { + node.mathspeakTemplate = localization.createMathspeakTemplate( + 'start-text', + 'end-text' + ); + } else if ('isStyleBlock' in node && node.isStyleBlock?.()) { + if ( + node.ariaLabel && + !('shouldNotSpeakDelimiters' in node && node.shouldNotSpeakDelimiters) + ) { + // Handle different style block types with specific messages + const ariaLabel = node.ariaLabel.toLowerCase().replace(/\s+/g, '-'); + const startKey = `start-${ariaLabel}`; + const endKey = `end-${ariaLabel}`; + + // Check if specific messages exist, otherwise use fallback + if ( + localization.hasMessage(startKey) && + localization.hasMessage(endKey) + ) { + node.mathspeakTemplate = localization.createMathspeakTemplate( + startKey, + endKey + ); + } else { + // Update the fallback template with localized generic messages + node.mathspeakTemplate = [ + localization.formatMessage('generic-start') + + ' ' + + node.ariaLabel + + ',', + ', ' + + localization.formatMessage('generic-end') + + ' ' + + node.ariaLabel + ]; + } + } + } + }); + } scrollHoriz() {} selectionChanged() {} setOverflowClasses() {} } + +/** + * Helper function for math elements to get their controller's localization + * + * This function traverses up the MQ node tree to find a controller with localization, + * providing a fallback to global localization if no controller is found. + * + * @param node - The MQNode to start the search from + * @returns A MathQuillLocalization instance for the found controller or global fallback + */ +function getControllerLocalization(node: MQNode) { + // Walk up the tree to find the controller + let current: MQNode | undefined = node; + while (current && !('controller' in current)) { + current = current.parent; + } + + // Try to get controller's localization using safe property access + if ( + current && + 'controller' in current && + current.controller && + typeof current.controller === 'object' && + 'getLocalizationForController' in current.controller && + typeof current.controller.getLocalizationForController === 'function' + ) { + return current.controller.getLocalizationForController(); + } + + // Fallback: create a localization instance with the current global language + // This can happen in tests or when math elements are created in isolation + // Always create a new instance to ensure we get the latest global language + return new MathQuillLocalization(getCurrentGlobalLanguage()); +} diff --git a/src/cursor.ts b/src/cursor.ts index 9fe3a03bb..52757bc20 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -38,7 +38,7 @@ class Cursor extends Point { ); private _domFrag = domFrag(); selection: MQSelection | undefined; - intervalId: number; + intervalId: TimeoutId; anticursor: Anticursor | undefined; constructor( diff --git a/src/locale/en/messages.ftl b/src/locale/en/messages.ftl new file mode 100644 index 000000000..1f2193195 --- /dev/null +++ b/src/locale/en/messages.ftl @@ -0,0 +1,290 @@ +# Text and Style Block Messages +start-text = Start Text +end-text = End Text + +# Generic fallback messages +generic-start = Start +generic-end = End + +# Bracket messages +bracket-left = left +bracket-right = right + +# Individual punctuation characters +left-parenthesis = left parenthesis +right-parenthesis = right parenthesis +left-bracket = left bracket +right-bracket = right bracket +left-brace = left brace +right-brace = right brace + +# Style block messages +start-italic-font = Start Italic Font +end-italic-font = End Italic Font +start-bold-font = Start Bold Font +end-bold-font = End Bold Font +start-roman-font = Start Roman Font +end-roman-font = End Roman Font +start-serif-font = Start Serif Font +end-serif-font = End Serif Font +start-math-text = Start Math Text +end-math-text = End Math Text +start-underline = Start Underline +end-underline = End Underline +start-overline = Start Overline +end-overline = End Overline +start-over-right-arrow = Start Over Right Arrow +end-over-right-arrow = End Over Right Arrow +start-over-left-arrow = Start Over Left Arrow +end-over-left-arrow = End Over Left Arrow +start-over-left-and-right-arrow = Start Over Left and Right Arrow +end-over-left-and-right-arrow = End Over Left and Right Arrow + +# Fraction-specific messages +start-fraction = Start Fraction +end-fraction = End Fraction +start-nested-fraction = Start Nested Fraction +nested-over = nested over +end-nested-fraction = End Nested Fraction +over = over + +# Root-specific messages +start-root = Start Root +end-root = End Root +start-cube-root = Start Cube Root +end-cube-root = End Cube Root +root-index = Root Index +start-nth-root = Start Root +end-nth-root = End Root + +# Other structures +start-absolute-value = Start Absolute Value +end-absolute-value = End Absolute Value +start-binomial = Start Binomial +end-binomial = End Binomial +choose = Choose + +# Summation and bounds +start-lower-bound = Start Lower Bound +end-lower-bound = End Lower Bound +start-upper-bound = Start Upper Bound +end-upper-bound = End Upper Bound + +# Binomial indices +start-upper-index = Start Upper Index +end-upper-index = End Upper Index +start-lower-index = Start Lower Index +end-lower-index = End Lower Index + +# Token blocks +start-token = Start Token +end-token = End Token + +# Subscript and Superscript +subscript = Subscript +superscript = Superscript +baseline = Baseline + +# Power expressions +power-squared = squared +power-cubed = cubed +power-ordinal = to the { $number }{ $number -> + [1] st + [21] st + [31] st + [41] st + [51] st + [61] st + [71] st + [81] st + [91] st + [2] nd + [22] nd + [32] nd + [42] nd + [52] nd + [62] nd + [72] nd + [82] nd + [92] nd + [3] rd + [23] rd + [33] rd + [43] rd + [53] rd + [63] rd + [73] rd + [83] rd + [93] rd + *[other] th +} power +power-zero = to the 0 power +power-negative = to the negative { $number }{ $number -> + [1] st + [21] st + [31] st + [41] st + [51] st + [61] st + [71] st + [81] st + [91] st + [2] nd + [22] nd + [32] nd + [42] nd + [52] nd + [62] nd + [72] nd + [82] nd + [92] nd + [3] rd + [23] rd + [33] rd + [43] rd + [53] rd + [63] rd + [73] rd + [83] rd + [93] rd + *[other] th +} power + +# Basic mathematical operators +plus = plus +positive = positive +minus = minus +negative = negative +times = times +dot = dot +equals = equals + +# Inequality operators +less-than = less than +greater-than = greater than +less-than-or-equal-to = less than or equal to +greater-than-or-equal-to = greater than or equal to +not-equal-to = not equal to +approximately-equal-to = approximately equal to + +# Mixed fractions +and = and + +# Fraction shortcuts with simple key-value pairs +fraction-shortcut-1-2 = 1 half +fraction-shortcut-1-3 = 1 third +fraction-shortcut-1-4 = 1 quarter +fraction-shortcut-1-5 = 1 fifth +fraction-shortcut-1-6 = 1 sixth +fraction-shortcut-1-7 = 1 seventh +fraction-shortcut-1-8 = 1 eighth +fraction-shortcut-1-9 = 1 ninth +fraction-shortcut-2-2 = 2 halves +fraction-shortcut-2-3 = 2 thirds +fraction-shortcut-2-4 = 2 quarters +fraction-shortcut-2-5 = 2 fifths +fraction-shortcut-2-6 = 2 sixths +fraction-shortcut-2-7 = 2 sevenths +fraction-shortcut-2-8 = 2 eighths +fraction-shortcut-2-9 = 2 ninths +fraction-shortcut-3-3 = 3 thirds +fraction-shortcut-3-4 = 3 quarters +fraction-shortcut-3-5 = 3 fifths +fraction-shortcut-3-6 = 3 sixths +fraction-shortcut-3-7 = 3 sevenths +fraction-shortcut-3-8 = 3 eighths +fraction-shortcut-3-9 = 3 ninths +fraction-shortcut-4-4 = 4 quarters +fraction-shortcut-4-5 = 4 fifths +fraction-shortcut-4-6 = 4 sixths +fraction-shortcut-4-7 = 4 sevenths +fraction-shortcut-4-8 = 4 eighths +fraction-shortcut-4-9 = 4 ninths +fraction-shortcut-5-5 = 5 fifths +fraction-shortcut-5-6 = 5 sixths +fraction-shortcut-5-7 = 5 sevenths +fraction-shortcut-5-8 = 5 eighths +fraction-shortcut-5-9 = 5 ninths +fraction-shortcut-6-6 = 6 sixths +fraction-shortcut-6-7 = 6 sevenths +fraction-shortcut-6-8 = 6 eighths +fraction-shortcut-6-9 = 6 ninths +fraction-shortcut-7-7 = 7 sevenths +fraction-shortcut-7-8 = 7 eighths +fraction-shortcut-7-9 = 7 ninths +fraction-shortcut-8-8 = 8 eighths +fraction-shortcut-8-9 = 8 ninths +fraction-shortcut-9-9 = 9 ninths + +# Fraction denominators (singular and plural forms) +fraction-denom-2-singular = half +fraction-denom-2-plural = halves +fraction-denom-3-singular = third +fraction-denom-3-plural = thirds +fraction-denom-4-singular = quarter +fraction-denom-4-plural = quarters +fraction-denom-5-singular = fifth +fraction-denom-5-plural = fifths +fraction-denom-6-singular = sixth +fraction-denom-6-plural = sixths +fraction-denom-7-singular = seventh +fraction-denom-7-plural = sevenths +fraction-denom-8-singular = eighth +fraction-denom-8-plural = eighths +fraction-denom-9-singular = ninth +fraction-denom-9-plural = ninths + +# Summation and Product Notation +start-sum = Start sum from +end-sum = end sum +start-product = Start product from +end-product = end product +start-coproduct = Start co product from +end-coproduct = end co product + +# Integrals +start-integral = Start integral from +end-integral = end integral + +# Consolidated "to" message (used by summation, product, coproduct, integral) +to = to + +# Directional navigation +before = before +after = after +beginning-of = beginning of +end-of = end of + +# Generic fallback messages +algebraic-fraction = { $numerator } over { $denominator } +algebraic-power = { $base } to the { $exponent } power +algebraic-root = { $index -> + [2] square root of { $radicand } + [3] cube root of { $radicand } + *[other] { $index } root of { $radicand } +} + +# Default ARIA labels +default-aria-label = Math Input + +# Auto operator names (can be overridden by user configuration) +auto-operator-sin = sine +auto-operator-cos = cosine +auto-operator-tan = tangent +auto-operator-sec = secant +auto-operator-csc = cosecant +auto-operator-cot = cotangent +auto-operator-sinh = hyperbolic sine +auto-operator-cosh = hyperbolic cosine +auto-operator-tanh = hyperbolic tangent +auto-operator-log = logarithm +auto-operator-ln = natural logarithm +auto-operator-lg = common logarithm +auto-operator-exp = exponential +auto-operator-lim = limit +auto-operator-sup = supremum +auto-operator-inf = infimum +auto-operator-max = maximum +auto-operator-min = minimum +auto-operator-gcd = greatest common divisor +auto-operator-standard-deviation = standard deviation \ No newline at end of file diff --git a/src/locale/es/messages.ftl b/src/locale/es/messages.ftl new file mode 100644 index 000000000..d3c24f4f4 --- /dev/null +++ b/src/locale/es/messages.ftl @@ -0,0 +1,243 @@ +# Text and Style Block Messages +start-text = Inicio Texto +end-text = Fin Texto + +# Generic fallback messages +generic-start = Inicio +generic-end = Final + +# Bracket messages +bracket-left = izquierdo +bracket-right = derecho + +# Individual punctuation characters +left-parenthesis = paréntesis izquierdo +right-parenthesis = paréntesis derecho +left-bracket = corchete izquierdo +right-bracket = corchete derecho +left-brace = llave izquierda +right-brace = llave derecha + +# Style block messages +start-italic-font = Inicio Fuente Cursiva +end-italic-font = Fin Fuente Cursiva +start-bold-font = Inicio Fuente Negrita +end-bold-font = Fin Fuente Negrita +start-roman-font = Inicio Fuente Romana +end-roman-font = Fin Fuente Romana +start-serif-font = Inicio Fuente Serif +end-serif-font = Fin Fuente Serif +start-math-text = Inicio Texto Matemático +end-math-text = Fin Texto Matemático +start-underline = Inicio Subrayado +end-underline = Fin Subrayado +start-overline = Inicio Sobrelineado +end-overline = Fin Sobrelineado +start-over-right-arrow = Inicio Flecha Superior Derecha +end-over-right-arrow = Fin Flecha Superior Derecha +start-over-left-arrow = Inicio Flecha Superior Izquierda +end-over-left-arrow = Fin Flecha Superior Izquierda +start-over-left-and-right-arrow = Inicio Flecha Superior Izquierda y Derecha +end-over-left-and-right-arrow = Fin Flecha Superior Izquierda y Derecha + +# Fraction-specific messages +start-fraction = Inicio Fracción +end-fraction = Fin Fracción +start-nested-fraction = Inicio Fracción Anidada +nested-over = sobre anidado +end-nested-fraction = Fin Fracción Anidada +over = sobre + +# Root-specific messages +start-root = Inicio Raíz +end-root = Fin Raíz +start-cube-root = Inicio Raíz Cúbica +end-cube-root = Fin Raíz Cúbica +root-index = Índice de Raíz +start-nth-root = Inicio Raíz +end-nth-root = Fin Raíz + +# Other structures +start-absolute-value = Inicio Valor Absoluto +end-absolute-value = Fin Valor Absoluto +start-binomial = Inicio Binomial +end-binomial = Fin Binomial +choose = Elige + +# Summation and bounds +start-lower-bound = Inicio Límite Inferior +end-lower-bound = Fin Límite Inferior +start-upper-bound = Inicio Límite Superior +end-upper-bound = Fin Límite Superior + +# Binomial indices +start-upper-index = Inicio Índice Superior +end-upper-index = Fin Índice Superior +start-lower-index = Inicio Índice Inferior +end-lower-index = Fin Índice Inferior + +# Token blocks +start-token = Inicio Token +end-token = Fin Token + +# Subscript and Superscript +subscript = Subíndice +superscript = Superíndice +baseline = Línea Base + +# Power expressions +power-squared = al cuadrado +power-cubed = al cubo +power-ordinal = a la { $number -> + [1] primera + [2] segunda + [3] tercera + [4] cuarta + [5] quinta + [6] sexta + [7] séptima + [8] octava + [9] novena + *[other] { $number }ª +} potencia +power-zero = a la 0 potencia +power-negative = a la potencia menos { $number } + +# Basic mathematical operators +plus = más +positive = positivo +minus = menos +negative = menos +times = por +dot = punto +equals = igual + +# Inequality operators +less-than = menor que +greater-than = mayor que +less-than-or-equal-to = menor o igual que +greater-than-or-equal-to = mayor o igual que +not-equal-to = no igual a +approximately-equal-to = aproximadamente igual a + +# Mixed fractions +and = y + +# Fraction shortcuts with simple key-value pairs +fraction-shortcut-1-2 = 1 medio +fraction-shortcut-1-3 = 1 tercio +fraction-shortcut-1-4 = 1 cuarto +fraction-shortcut-1-5 = 1 quinto +fraction-shortcut-1-6 = 1 sexto +fraction-shortcut-1-7 = 1 séptimo +fraction-shortcut-1-8 = 1 octavo +fraction-shortcut-1-9 = 1 noveno +fraction-shortcut-2-2 = 2 medios +fraction-shortcut-2-3 = 2 tercios +fraction-shortcut-2-4 = 2 cuartos +fraction-shortcut-2-5 = 2 quintos +fraction-shortcut-2-6 = 2 sextos +fraction-shortcut-2-7 = 2 séptimos +fraction-shortcut-2-8 = 2 octavos +fraction-shortcut-2-9 = 2 novenos +fraction-shortcut-3-3 = 3 tercios +fraction-shortcut-3-4 = 3 cuartos +fraction-shortcut-3-5 = 3 quintos +fraction-shortcut-3-6 = 3 sextos +fraction-shortcut-3-7 = 3 séptimos +fraction-shortcut-3-8 = 3 octavos +fraction-shortcut-3-9 = 3 novenos +fraction-shortcut-4-4 = 4 cuartos +fraction-shortcut-4-5 = 4 quintos +fraction-shortcut-4-6 = 4 sextos +fraction-shortcut-4-7 = 4 séptimos +fraction-shortcut-4-8 = 4 octavos +fraction-shortcut-4-9 = 4 novenos +fraction-shortcut-5-5 = 5 quintos +fraction-shortcut-5-6 = 5 sextos +fraction-shortcut-5-7 = 5 séptimos +fraction-shortcut-5-8 = 5 octavos +fraction-shortcut-5-9 = 5 novenos +fraction-shortcut-6-6 = 6 sextos +fraction-shortcut-6-7 = 6 séptimos +fraction-shortcut-6-8 = 6 octavos +fraction-shortcut-6-9 = 6 novenos +fraction-shortcut-7-7 = 7 séptimos +fraction-shortcut-7-8 = 7 octavos +fraction-shortcut-7-9 = 7 novenos +fraction-shortcut-8-8 = 8 octavos +fraction-shortcut-8-9 = 8 novenos +fraction-shortcut-9-9 = 9 novenos + +# Fraction denominators (singular and plural forms) +fraction-denom-2-singular = medio +fraction-denom-2-plural = medios +fraction-denom-3-singular = tercio +fraction-denom-3-plural = tercios +fraction-denom-4-singular = cuarto +fraction-denom-4-plural = cuartos +fraction-denom-5-singular = quinto +fraction-denom-5-plural = quintos +fraction-denom-6-singular = sexto +fraction-denom-6-plural = sextos +fraction-denom-7-singular = séptimo +fraction-denom-7-plural = séptimos +fraction-denom-8-singular = octavo +fraction-denom-8-plural = octavos +fraction-denom-9-singular = noveno +fraction-denom-9-plural = novenos + +# Summation and Product Notation +start-sum = Inicio suma desde +end-sum = fin suma +start-product = Inicio producto desde +end-product = fin producto +start-coproduct = Inicio co producto desde +end-coproduct = fin co producto + +# Integrals +start-integral = Inicio integral desde +end-integral = fin integral + +# Consolidated "to" message (used by summation, product, coproduct, integral) +to = hasta + +# Directional navigation +before = antes +after = después +beginning-of = principio de +end-of = final de + +# Generic fallback messages +algebraic-fraction = { $numerator } sobre { $denominator } +algebraic-power = { $base } a la { $exponent } potencia +algebraic-root = { $index -> + [2] raíz cuadrada de { $radicand } + [3] raíz cúbica de { $radicand } + *[other] raíz { $index } de { $radicand } +} + +# Default ARIA labels +default-aria-label = Entrada Matemática + +# Auto operator names (can be overridden by user configuration) +auto-operator-sin = seno +auto-operator-cos = coseno +auto-operator-tan = tangente +auto-operator-sec = secante +auto-operator-csc = cosecante +auto-operator-cot = cotangente +auto-operator-sinh = seno hiperbólico +auto-operator-cosh = coseno hiperbólico +auto-operator-tanh = tangente hiperbólica +auto-operator-log = logaritmo +auto-operator-ln = logaritmo natural +auto-operator-lg = logaritmo común +auto-operator-exp = exponencial +auto-operator-lim = límite +auto-operator-sup = supremo +auto-operator-inf = ínfimo +auto-operator-max = máximo +auto-operator-min = mínimo +auto-operator-gcd = máximo común divisor +auto-operator-std = desviación estándar \ No newline at end of file diff --git a/src/mathquill.d.ts b/src/mathquill.d.ts index 96fc9f737..ba202499b 100644 --- a/src/mathquill.d.ts +++ b/src/mathquill.d.ts @@ -133,6 +133,7 @@ declare namespace MathQuill { prefixOperatorNames?: string; autoCommands?: string; logAriaAlerts?: boolean; + language?: string; autoParenthesizedFunctions?: string; quietEmptyDelimiters?: string; disableAutoSubstitutionInSubscripts?: boolean | { except: string }; diff --git a/src/publicapi.ts b/src/publicapi.ts index 27841e906..25de96959 100644 --- a/src/publicapi.ts +++ b/src/publicapi.ts @@ -62,7 +62,8 @@ const processedOptions = { leftRightIntoCmdGoes: true, maxDepth: true, interpretTildeAsSim: true, - disableAutoSubstitutionInSubscripts: true + disableAutoSubstitutionInSubscripts: true, + language: true }; type ProcessedOption = keyof typeof processedOptions; @@ -113,6 +114,7 @@ class Options { disableCopyPaste?: boolean; statelessClipboard?: boolean; logAriaAlerts?: boolean; + language?: string; onPaste?: () => void; onCut?: () => void; overrideTypedText?: (text: string) => void; @@ -250,6 +252,13 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API { var value = (newOptions as any)[name]; // TODO - think about typing this better var processor = (optionProcessors as any)[name]; // TODO - validate option processors better (currentOptions as any)[name] = processor ? processor(value) : value; // TODO - think about typing better + + // Handle language changes by updating the global language manager + if (name === 'language') { + // Always fall back gracefully for unsupported languages + // Log a warning but don't throw errors + setGlobalLanguage(value, false); + } } } } @@ -295,6 +304,10 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API { this.revert = function () { ctrlr.removeMouseEventListener(); + // Clean up language change callback + if (ctrlr.unregisterLanguageChange) { + ctrlr.unregisterLanguageChange(); + } domFrag(el) .removeClass('mq-editable-field mq-math-mode mq-text-mode') .empty() @@ -310,11 +323,37 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API { getAriaLabel() { return this.__controller.getAriaLabel(); } - config(opts: ConfigOptions) { + config(opts: ConfigOptions, isInitialConfig?: boolean) { config(this.__options, opts); if (opts.tabindex !== undefined) { this.__controller.setTabindex(opts.tabindex); } + if (opts.language !== undefined) { + // Update this controller's language directly + this.__controller.language = opts.language; + // Mark as explicit if this is NOT initial config (i.e., user called config() later) + // OR if this is initial config and the user provided the language option explicitly + if (!isInitialConfig) { + (this.__controller as any).explicitLanguage = true; + } else if (isInitialConfig && opts.hasOwnProperty('language')) { + // During initial config, only mark as explicit if language was actually provided by user + (this.__controller as any).explicitLanguage = true; + } + + // Update the controller's language and localization + if (this.__controller.setLanguage) { + this.__controller.setLanguage(opts.language); + } + + // Update the global language manager to ensure consistency + // This ensures that any fallback calls to getControllerLocalization work correctly + setGlobalLanguage(opts.language); + + // Update the aria label when language changes + if (this.__controller.updateAriaLabel) { + this.__controller.updateAriaLabel(); + } + } return this; } el() { @@ -541,9 +580,46 @@ function getInterface(v: number): MathQuill.v3.API | MathQuill.v1.API { MQ.config = function (opts: ConfigOptions) { config(BaseOptions.prototype, opts); + if (opts.language !== undefined) { + // Update the global language manager with the new language + setGlobalLanguage(opts.language); + } return this; }; + // Localization API - accessible only through MathQuill + MQ.L10N = { + // Create a new localization instance for a specific language + create: function (language: string) { + return new MathQuillLocalization(language); + }, + + // Set the global default language for new MathQuill instances + setLanguage: function (language: string) { + setGlobalLanguage(language); + }, + + // Get the current global default language + getLanguage: function () { + return getCurrentGlobalLanguage(); + }, + + // Register a callback for global language changes + onLanguageChange: function (listener: () => void) { + return onGlobalLanguageChange(listener); + }, + + // Check if a language is supported + isLanguageSupported: function (language: string) { + return MathQuillLocalization.isLanguageSupported(language); + }, + + // Resolve a language code to a supported language + resolveLanguage: function (language: string) { + return MathQuillLocalization.resolveLanguage(language); + } + }; + MQ.registerEmbed = function ( name: string, options: (data: EmbedOptionsData) => EmbedOptions diff --git a/src/services/aria.ts b/src/services/aria.ts index 7e5df7520..fc84c5cea 100755 --- a/src/services/aria.ts +++ b/src/services/aria.ts @@ -64,11 +64,21 @@ class Aria { } queueDirOf(dir: Direction) { prayDirection(dir); - return this.queue(dir === L ? 'before' : 'after'); + const localization = this.controller.getLocalizationForController(); + return this.queue( + dir === L + ? localization.formatMessage('before') + : localization.formatMessage('after') + ); } queueDirEndOf(dir: Direction) { prayDirection(dir); - return this.queue(dir === L ? 'beginning of' : 'end of'); + const localization = this.controller.getLocalizationForController(); + return this.queue( + dir === L + ? localization.formatMessage('beginning-of') + : localization.formatMessage('end-of') + ); } alert(t?: AriaQueueItem) { diff --git a/src/services/focusBlur.ts b/src/services/focusBlur.ts index 9cb549014..df8a95b4c 100644 --- a/src/services/focusBlur.ts +++ b/src/services/focusBlur.ts @@ -17,8 +17,8 @@ ControllerBase.onNotify(function (cursor, e) { class Controller_focusBlur extends Controller_exportText { blurred: boolean; - __disableGroupingTimeout: number; - textareaSelectionTimeout: number; + __disableGroupingTimeout: TimeoutId; + textareaSelectionTimeout: TimeoutId; disableGroupingForSeconds(seconds: number) { clearTimeout(this.__disableGroupingTimeout); @@ -32,7 +32,7 @@ class Controller_focusBlur extends Controller_exportText { } } - private blurTimeout: number; + private blurTimeout: TimeoutId; private handleTextareaFocusEditable = () => { const cursor = this.cursor; diff --git a/src/services/localization.ts b/src/services/localization.ts new file mode 100644 index 000000000..81643464c --- /dev/null +++ b/src/services/localization.ts @@ -0,0 +1,448 @@ +/** + * Fluent-based localization service for MathQuill screen reader announcements + * Provides centralized internationalization for mathematical expression descriptions + * + * Features: + * - Type-safe Fluent integration with proper TypeScript definitions + * - Graceful language fallback with console warnings for unsupported languages + * - Global language state management with change callbacks + * - Caching of localization bundles for performance + * - Support for language variants (e.g., 'en-US' → 'en') + */ + +// TypeScript interfaces for Fluent types that match @fluent/bundle +// These are provided by fluent-bundle.js which is included in the build +// Using local interfaces instead of imports to maintain compatibility with concatenated build + +type FluentVariable = string | number | Date | boolean; + +interface FluentMessage { + value?: any; // Pattern from Fluent AST - opaque type handled by Fluent internals + attributes?: Record; +} + +interface FluentBundleType { + locales: Array; + hasMessage(id: string): boolean; + getMessage(id: string): FluentMessage | undefined; + addResource(resource: FluentResourceType): Array; + formatPattern( + pattern: any, // Fluent Pattern type - opaque implementation detail + args?: Record | null, + errors?: Array | null + ): string; +} + +interface FluentResourceType { + body: Array; // Fluent AST nodes - opaque implementation detail +} + +declare var FluentBundle: { + new ( + locales: string | Array, + options?: { + functions?: Record; + useIsolating?: boolean; + transform?: (text: string) => string; + } + ): FluentBundleType; +}; +declare var FluentResource: { + new (source: string): FluentResourceType; +}; +declare var parseResource: any; + +// Note: These functions are provided by locale-imports.ts +// They are included in the build before this file + +class MathQuillLocalization { + private bundle: FluentBundleType | null = null; + private requestedLanguage: string = 'en'; + private resolvedLanguage: string = 'en'; + private bundleCache: Map = new Map(); + private languageChangeCallbacks: Set<() => void> = new Set(); + + constructor(language: string = 'en') { + this.setLanguage(language); + } + + setLanguage(language: string, throwOnInvalid: boolean = false) { + this.requestedLanguage = language; + + // Check if the language is completely invalid (not a valid language code format) + if (throwOnInvalid && !isValidLanguageCode(language)) { + throw new Error( + `Unsupported language code: ${language}. Supported languages: en, es` + ); + } + + // Resolve the language to the actual language that will be loaded + const resolvedLanguage = getResolvedLanguage(language); + const previousLanguage = this.resolvedLanguage; + this.resolvedLanguage = resolvedLanguage; + + // Check cache first (use resolved language for caching) + if (this.bundleCache.has(resolvedLanguage)) { + this.bundle = this.bundleCache.get(resolvedLanguage)!; + if (resolvedLanguage !== language) { + console.info( + `Language '${language}' resolved to '${resolvedLanguage}' (cached)` + ); + } + // Trigger callbacks if language actually changed + if (previousLanguage !== resolvedLanguage) { + this.triggerLanguageChangeCallbacks(); + } + return; + } + + try { + // Try to load the resolved language file + const messages = this.loadMessages(resolvedLanguage); + this.bundle = this.createFluentBundle(resolvedLanguage, messages); + this.bundleCache.set(resolvedLanguage, this.bundle); + + if (resolvedLanguage !== language) { + console.info( + `Language '${language}' resolved to '${resolvedLanguage}'` + ); + } + + // Trigger callbacks if language actually changed + if (previousLanguage !== resolvedLanguage) { + this.triggerLanguageChangeCallbacks(); + } + } catch (error) { + console.warn( + `Failed to load language ${resolvedLanguage}, falling back to English` + ); + if (resolvedLanguage !== 'en') { + this.setLanguage('en'); + } else { + // If even English fails, we have a serious problem + throw new Error('Failed to load fallback English language'); + } + } + } + + private createFluentBundle( + language: string, + ftlContent: string + ): FluentBundleType { + // FluentBundle constructor expects a locale or array of locales + // Disable Unicode isolation marks for cleaner screen reader output + const bundle = new FluentBundle([language], { useIsolating: false }); + + // Create a FluentResource from the raw FTL content + // FluentResource constructor takes the raw string content, not parsed AST + const fluentResource = new FluentResource(ftlContent); + + const errors = bundle.addResource(fluentResource); + if (errors.length > 0) { + console.warn(`Fluent bundle errors for ${language}:`, errors); + } + + return bundle; + } + + private loadMessages(language: string): string { + // Load messages using the fallback-aware loader + const messages = loadLocaleMessages(language); + if (!messages) { + throw new Error(`No messages found for language: ${language}`); + } + return messages; + } + + formatMessage(id: string, args?: Record): string { + if (!this.bundle) { + console.warn('Localization not initialized'); + return id; + } + + const message = this.bundle.getMessage(id); + if (!message || !message.value) { + console.warn(`Missing localization message: ${id}`); + return id; + } + + const errors: Error[] = []; + const formatted = this.bundle.formatPattern(message.value, args, errors); + + if (errors.length > 0) { + console.warn(`Formatting errors for message '${id}':`, errors); + } + + // Trim whitespace to ensure clean mathspeak output + return formatted.trim(); + } + + // Convenience methods for common mathematical structures + + formatFractionShortcut(numerator: number, denominator: number): string { + const key = `fraction-shortcut-${numerator}-${denominator}`; + const shortcut = this.formatMessage(key); + + // If the specific key exists, use it; otherwise return empty string to fall back to full form + if (shortcut !== key) { + return shortcut; + } + return ''; + } + + formatFractionDenominator(numerator: number, denominator: number): string { + // Use singular form for |numerator| = 1, plural for |numerator| > 1 + const form = Math.abs(numerator) === 1 ? 'singular' : 'plural'; + const key = `fraction-denom-${denominator}-${form}`; + const result = this.formatMessage(key); + + // If the specific key exists, use it; otherwise return empty string to fall back to full form + if (result !== key) { + return result; + } + return ''; + } + + formatPowerExpression(number: number): string { + if (number === 2) { + return this.formatMessage('power-squared'); + } else if (number === 3) { + return this.formatMessage('power-cubed'); + } else if (number === 0) { + return this.formatMessage('power-zero'); + } else if (number < 0) { + // Handle negative exponents: "to the negative 5th power" instead of "to the -5th power" + const positiveNumber = Math.abs(number); + + // First try with number to get proper ordinal translations + const resultWithNumber = this.formatMessage('power-negative', { + number: positiveNumber + }); + + // Check if Fluent applied unwanted formatting (commas, spaces, etc.) + const numberStr = positiveNumber.toString(); + const hasNumberFormatting = !resultWithNumber.includes(numberStr); + + if (hasNumberFormatting && numberStr.length > 3) { + // Fall back to string format to prevent unwanted formatting + return this.formatMessage('power-negative', { number: numberStr }); + } + + return resultWithNumber; + } else { + // First try with number to get proper ordinal translations + const resultWithNumber = this.formatMessage('power-ordinal', { number }); + + // Check if Fluent applied unwanted formatting (commas, spaces, etc.) + const numberStr = number.toString(); + const hasNumberFormatting = !resultWithNumber.includes(numberStr); + + if (hasNumberFormatting && numberStr.length > 3) { + // Fall back to string format to prevent unwanted formatting + return this.formatMessage('power-ordinal', { number: numberStr }); + } + + return resultWithNumber; + } + } + + // Auto operator localization + formatAutoOperator(operatorName: string): string { + // First, try to find a localized version + const key = `auto-operator-${operatorName}`; + if (this.bundle?.getMessage(key)) { + return this.formatMessage(key); + } + + // If no localization exists, check if the operator contains | syntax + // This supports user-defined operators like "sin|sine" + if (operatorName.includes('|')) { + const parts = operatorName.split('|'); + return parts[1] || parts[0]; // Return spoken part (after |) or fall back to visual part + } + + // Fallback to original name if no localization exists + return operatorName; + } + + /** + * Creates a mathspeak template for delimited structures + * Supports both 2-parameter (start/end) and 3-parameter (start/middle/end) templates + * @param startKey - The localization key for the start delimiter + * @param middleOrEndKey - The localization key for middle part (3-param) or end delimiter (2-param) + * @param endKey - The localization key for the end delimiter (only for 3-param version) + * @returns Array of template strings with commas for screen reader pauses + */ + createMathspeakTemplate( + startKey: string, + middleOrEndKey: string, + endKey?: string + ): string[] { + if (endKey !== undefined) { + // 3-parameter version: start, middle, end + return [ + this.formatMessage(startKey) + ',', + ' ' + this.formatMessage(middleOrEndKey) + ' ', + ', ' + this.formatMessage(endKey) + ]; + } else { + // 2-parameter version: start, end + return [ + this.formatMessage(startKey) + ',', + ', ' + this.formatMessage(middleOrEndKey) + ]; + } + } + + getCurrentLanguage(): string { + return this.requestedLanguage; + } + + /** + * Check if a message key exists in the current bundle + * @param messageKey - The message key to check + * @returns True if the message exists, false otherwise + */ + hasMessage(messageKey: string): boolean { + return this.bundle?.hasMessage(messageKey) || false; + } + + getResolvedLanguage(): string { + return this.resolvedLanguage; + } + + /** + * Register a callback to be called when the language changes + * @param callback Function to call when language changes + * @returns Function to unregister the callback + */ + onLanguageChange(callback: () => void): () => void { + this.languageChangeCallbacks.add(callback); + return () => { + this.languageChangeCallbacks.delete(callback); + }; + } + + /** + * Trigger all registered language change callbacks + */ + private triggerLanguageChangeCallbacks() { + this.languageChangeCallbacks.forEach((callback) => { + try { + callback(); + } catch (error) { + console.warn('Error in language change callback:', error); + } + }); + } + + /** + * Check if a language is supported (with fallback resolution) + */ + static isLanguageSupported(language: string): boolean { + return hasLanguageSupport(language); + } + + /** + * Get the language that would actually be loaded for a given request + */ + static resolveLanguage(language: string): string { + return getResolvedLanguage(language); + } +} + +/** + * Validates if a language code has a reasonable format. + * Only rejects completely invalid language codes, not unsupported ones. + */ +function isValidLanguageCode(language: string): boolean { + if (!language || typeof language !== 'string') { + return false; + } + + // Basic language code format validation (ISO 639-1 and variants) + // Accept: en, es, fr, fr-CA, en-US, zh-CN, etc. + // Reject: xx, 123, empty string, special characters, etc. + const languagePattern = /^[a-z]{2,3}(-[A-Z]{2})?$/i; + return languagePattern.test(language); +} + +// Global language state manager for coordinating language changes across controllers +class GlobalLanguageManager { + private currentLanguage: string = 'en'; + private listeners: (() => void)[] = []; + + setLanguage(language: string, throwOnError: boolean = false): void { + const resolvedLanguage = MathQuillLocalization.resolveLanguage(language); + + // Warn about unsupported languages, but don't throw errors unless explicitly requested + if (!MathQuillLocalization.isLanguageSupported(language)) { + if (throwOnError) { + throw new Error(`Language "${language}" is not supported`); + } else { + console.warn( + `Language "${language}" is not supported, falling back to "${resolvedLanguage}"` + ); + } + } + + if (this.currentLanguage !== resolvedLanguage) { + this.currentLanguage = resolvedLanguage; + this.notifyListeners(); + } + } + + getCurrentLanguage(): string { + return this.currentLanguage; + } + + onLanguageChange(listener: () => void): () => void { + this.listeners.push(listener); + return () => { + const index = this.listeners.indexOf(listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } + }; + } + + private notifyListeners(): void { + this.listeners.forEach((listener) => listener()); + } +} + +// Global language manager instance +const globalLanguageManager = new GlobalLanguageManager(); + +// Global language management functions +// These functions are made available globally through the concatenated build system +// and can be accessed by other modules within MathQuill + +/** + * Sets the global default language for new MathQuill instances + * @param language - Language code (e.g., 'en', 'es', 'en-US') + * @param throwOnError - Whether to throw errors for unsupported languages (default: false) + */ +function setGlobalLanguage( + language: string, + throwOnError: boolean = false +): void { + globalLanguageManager.setLanguage(language, throwOnError); +} + +/** + * Gets the current global default language + * @returns The current language code + */ +function getCurrentGlobalLanguage(): string { + return globalLanguageManager.getCurrentLanguage(); +} + +/** + * Registers a callback for global language changes + * @param listener - Function to call when language changes + * @returns Function to unregister the listener + */ +function onGlobalLanguageChange(listener: () => void): () => void { + return globalLanguageManager.onLanguageChange(listener); +} diff --git a/src/services/saneKeyboardEvents.util.ts b/src/services/saneKeyboardEvents.util.ts index 8aa8964fe..3c93de047 100644 --- a/src/services/saneKeyboardEvents.util.ts +++ b/src/services/saneKeyboardEvents.util.ts @@ -1,6 +1,6 @@ /** Poller that fires once every tick. */ class EveryTick { - private timeoutId: number; + private timeoutId: TimeoutId; private fn: (...args: Args | []) => void = noop; constructor() {} diff --git a/src/shared_types.d.ts b/src/shared_types.d.ts index 0f90252dc..7db110d44 100644 --- a/src/shared_types.d.ts +++ b/src/shared_types.d.ts @@ -1,4 +1,11 @@ +/** + * Shared TypeScript type definitions for MathQuill + */ + type NodeRef = MQNode | 0; + +// Handle timeout types for both browser and Node.js environments +type TimeoutId = number | NodeJS.Timeout; type ControllerEvent = | 'move' | 'upDown' diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index 012eb2ae2..0c2d94f1b 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -169,16 +169,16 @@ suite('aria', function () { // We have logic to shorten the speak we return for common numeric fractions and superscripts. // While editing, however, the slightly longer form (but unambiguous) form of the item should be spoken. // In this case, we would shorten the fraction 1/2 to "1 half" when reading, - // but navigating around the equation should result in "StartFraction, 1 Over 2, EndFraction." + // but navigating around the equation should result in "Start Fraction, 1 over 2, End Fraction." mathField.keystroke('Tab'); - assertAriaEqual('after StartFraction, 1 Over 2 , EndFraction'); + assertAriaEqual('after Start Fraction, 1 over 2 , End Fraction'); mathField.keystroke('Backspace'); assertAriaEqual('end of denominator 2'); mathField.keystroke('Backspace'); assertAriaEqual('2'); mathField.keystroke('Backspace'); - assertAriaEqual('Over'); + assertAriaEqual('over'); mathField.keystroke('Backspace'); assertAriaEqual('1'); }); @@ -206,7 +206,7 @@ suite('aria', function () { assertAriaEqual('2'); mathField.keystroke('Tab'); - assertAriaEqual('after StartBinomial, 1 Choose 2 , EndBinomial'); + assertAriaEqual('after Start Binomial, 1 Choose 2 , End Binomial'); mathField.keystroke('Backspace'); assertAriaEqual('end of lower index 2'); @@ -292,13 +292,13 @@ suite('aria', function () { )[0] ); assert.equal( - '"y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', + '"y" equals Start Fraction, 2 "x" over 3 "y" , End Fraction', staticMath.__controller.mathspeakSpan.textContent ); assert.equal('', staticMath.getAriaLabel()); staticMath.setAriaLabel('Static Label'); assert.equal( - 'Static Label: "y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', + 'Static Label: "y" equals Start Fraction, 2 "x" over 3 "y" , End Fraction', staticMath.__controller.mathspeakSpan.textContent ); assert.equal('Static Label', staticMath.getAriaLabel()); diff --git a/test/unit/localization-fluent.test.js b/test/unit/localization-fluent.test.js new file mode 100644 index 000000000..ac464c455 --- /dev/null +++ b/test/unit/localization-fluent.test.js @@ -0,0 +1,50 @@ +suite('Localization Fluent Integration', function () { + var $ = window.test_only_jquery; + + setup(function () { + // Reset to English before each test + var localization = MQ.L10N.create('en'); + localization.setLanguage('en'); + }); + + test('parses FTL syntax correctly', function () { + var localization = MQ.L10N.create('en'); + + // Test parametric message parsing + var result = localization.formatMessage('power-ordinal', { number: 5 }); + assert.equal(result, 'to the 5th power'); + + // Test in Spanish + localization.setLanguage('es'); + result = localization.formatMessage('power-ordinal', { number: 5 }); + assert.equal(result, 'a la quinta potencia'); + }); + + test('handles missing parameters gracefully', function () { + var localization = MQ.L10N.create('en'); + + // Try to format a parametric message without providing parameters + var result = localization.formatMessage('power-ordinal'); + + // Should handle missing parameters without crashing by returning the raw formatted string. We might want to make this an error at some point. + assert.equal(result, 'to the {$number}th power'); + }); + + test('handles malformed FTL gracefully', function () { + // This test ensures the system doesn't crash on malformed FTL + // Since we control the FTL files, this is more of a safety net + var localization = MQ.L10N.create('en'); + + // Try to format a non-existent message + var result = localization.formatMessage('completely-nonexistent-message'); + assert.equal(result, 'completely-nonexistent-message'); + }); + + test('handles fallback on bundle creation failure', function () { + var localization = MQ.L10N.create('en'); + + // Try to set an unsupported language + localization.setLanguage('xyz'); // Should fall back to English + assert.equal(localization.getResolvedLanguage(), 'en'); + }); +}); diff --git a/test/unit/localization-mathspeak.test.js b/test/unit/localization-mathspeak.test.js new file mode 100644 index 000000000..f66279d4d --- /dev/null +++ b/test/unit/localization-mathspeak.test.js @@ -0,0 +1,221 @@ +suite('Localization Mathspeak Integration', function () { + var $ = window.test_only_jquery; + + setup(function () { + // Reset to English before each test + MQ.L10N.setLanguage('en'); + }); + + suite('Mathspeak Recomputation on Language Change', function () { + test('updates mathspeak when language changes', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Type a fraction + mq.typedText('1/2'); + + // Get English mathspeak + var englishMathspeak = mq.mathspeak(); + assert.equal(englishMathspeak, '1 half'); + + // Switch to Spanish + MQ.L10N.setLanguage('es'); + + // Get Spanish mathspeak + var spanishMathspeak = mq.mathspeak(); + assert.equal(spanishMathspeak, '1 medio'); + }); + + test('updates aria label when language changes', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Check default English aria label + var englishLabel = mq.getAriaLabel(); + assert.equal(englishLabel, 'Math Input'); + + // Switch to Spanish + MQ.L10N.setLanguage('es'); + + // Check Spanish aria label + var spanishLabel = mq.getAriaLabel(); + assert.equal(spanishLabel, 'Entrada Matemática'); + }); + + test('preserves custom aria labels during language changes', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Set custom aria label + mq.setAriaLabel('Expression 1'); + assert.equal(mq.getAriaLabel(), 'Expression 1'); + + // Switch language + MQ.L10N.setLanguage('es'); + + // Custom label should be preserved + assert.equal(mq.getAriaLabel(), 'Expression 1'); + }); + }); + + suite('Aria Label Key Normalization', function () { + test('normalizes aria label keys with hyphens', function () { + var localization = MQ.L10N.create('en'); + + // Test that spaces are converted to hyphens in aria label keys + assert.equal( + localization.formatMessage('start-lower-bound'), + 'Start Lower Bound' + ); + assert.equal( + localization.formatMessage('end-upper-bound'), + 'End Upper Bound' + ); + }); + + test('normalizes aria label keys in Spanish', function () { + MQ.L10N.setLanguage('es'); + var localization = MQ.L10N.create('es'); + + assert.equal( + localization.formatMessage('start-lower-bound'), + 'Inicio Límite Inferior' + ); + assert.equal( + localization.formatMessage('end-upper-bound'), + 'Fin Límite Superior' + ); + }); + }); + + suite('Fraction Denominator Pluralization', function () { + test('uses singular forms for numerator = 1 in English', function () { + var localization = MQ.L10N.create('en'); + localization.setLanguage('en'); + + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + mq.latex('\\frac{1}{2}'); + assert.equal(mq.mathspeak(), '1 half'); + mq.latex('\\frac{1}{3}'); + assert.equal(mq.mathspeak(), '1 third'); + mq.latex('\\frac{1}{4}'); + assert.equal(mq.mathspeak(), '1 quarter'); + }); + + test('uses plural forms for numerator > 1 in English', function () { + var localization = MQ.L10N.create('en'); + localization.setLanguage('en'); + + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + mq.latex('\\frac{2}{2}'); + assert.equal(mq.mathspeak(), '2 halves'); + mq.latex('\\frac{3}{4}'); + assert.equal(mq.mathspeak(), '3 quarters'); + mq.latex('\\frac{5}{6}'); + assert.equal(mq.mathspeak(), '5 sixths'); + }); + + test('uses singular forms for numerator = 1 in Spanish', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + MQ.L10N.setLanguage('es'); + + mq.latex('\\frac{1}{2}'); + assert.equal(mq.mathspeak(), '1 medio'); + mq.latex('\\frac{1}{3}'); + assert.equal(mq.mathspeak(), '1 tercio'); + mq.latex('\\frac{1}{4}'); + assert.equal(mq.mathspeak(), '1 cuarto'); + }); + + test('uses plural forms for numerator > 1 in Spanish', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + MQ.L10N.setLanguage('es'); + + mq.latex('\\frac{2}{2}'); + assert.equal(mq.mathspeak(), '2 medios'); + mq.latex('\\frac{3}{4}'); + assert.equal(mq.mathspeak(), '3 cuartos'); + mq.latex('\\frac{5}{6}'); + assert.equal(mq.mathspeak(), '5 sextos'); + }); + + test('negative numerators in English', function () { + var localization = MQ.L10N.create('en'); + localization.setLanguage('en'); + + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + mq.latex('\\frac{-1}{2}'); + assert.equal(mq.mathspeak(), 'negative 1 half'); + mq.latex('\\frac{-1}{3}'); + assert.equal(mq.mathspeak(), 'negative 1 third'); + mq.latex('\\frac{-2}{3}'); + assert.equal(mq.mathspeak(), 'negative 2 thirds'); + mq.latex('\\frac{-3}{4}'); + assert.equal(mq.mathspeak(), 'negative 3 quarters'); + }); + + test('negative numerators in Spanish', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + MQ.L10N.setLanguage('es'); + + mq.latex('\\frac{-1}{2}'); + assert.equal(mq.mathspeak(), 'menos 1 medio'); + mq.latex('\\frac{-1}{3}'); + assert.equal(mq.mathspeak(), 'menos 1 tercio'); + mq.latex('\\frac{-2}{3}'); + assert.equal(mq.mathspeak(), 'menos 2 tercios'); + + mq.latex('\\frac{-3}{4}'); + assert.equal(mq.mathspeak(), 'menos 3 cuartos'); + }); + }); + + suite('Power Expression Number Formatting', function () { + test('English exponents', function () { + MQ.L10N.setLanguage('en'); + + var mq = MQ.MathField($('').appendTo('#mock')[0]); + mq.latex('x^{1}'); + assert.equal(mq.mathspeak(), '"x" to the 1st power'); + mq.latex('x^{2}'); + assert.equal(mq.mathspeak(), '"x" squared'); + mq.latex('x^{3}'); + assert.equal(mq.mathspeak(), '"x" cubed'); + mq.latex('x^{1000}'); + assert.equal(mq.mathspeak(), '"x" to the 1000th power'); + mq.latex('x^{12345}'); + assert.equal(mq.mathspeak(), '"x" to the 12345th power'); + }); + + test('Spanish exponents', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + MQ.L10N.setLanguage('es'); + + mq.latex('x^4'); + assert.equal(mq.mathspeak(), '"x" a la cuarta potencia'); + mq.latex('x^5'); + assert.equal(mq.mathspeak(), '"x" a la quinta potencia'); + mq.latex('x^{1000}'); + assert.equal(mq.mathspeak(), '"x" a la 1000ª potencia'); + mq.latex('x^{12345}'); + assert.equal(mq.mathspeak(), '"x" a la 12345ª potencia'); + }); + + test('negative exponents', function () { + MQ.L10N.setLanguage('en'); + + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + mq.latex('10^{-5}'); + assert.equal(mq.mathspeak(), '10 to the negative 5th power'); + mq.latex('x^{-2}'); + assert.equal(mq.mathspeak(), '"x" to the negative 2nd power'); + + // Switch to Spanish + MQ.L10N.setLanguage('es'); + + mq.latex('10^{-5}'); + assert.equal(mq.mathspeak(), '10 a la potencia menos 5'); + }); + }); +}); diff --git a/test/unit/localization.test.js b/test/unit/localization.test.js new file mode 100644 index 000000000..d44900e4a --- /dev/null +++ b/test/unit/localization.test.js @@ -0,0 +1,225 @@ +suite('Localization', function () { + var $ = window.test_only_jquery; + + setup(function () { + // Reset to English before each test + MQ.L10N.setLanguage('en'); + }); + + suite('Basic Message Formatting', function () { + test('formats basic messages in English', function () { + var localization = MQ.L10N.create('en'); + assert.equal(localization.formatMessage('plus'), 'plus'); + assert.equal(localization.formatMessage('minus'), 'minus'); + assert.equal(localization.formatMessage('times'), 'times'); + assert.equal(localization.formatMessage('over'), 'over'); + }); + + test('formats basic messages in Spanish', function () { + var localization = MQ.L10N.create('es'); + + assert.equal(localization.formatMessage('plus'), 'más'); + assert.equal(localization.formatMessage('minus'), 'menos'); + assert.equal(localization.formatMessage('times'), 'por'); + assert.equal(localization.formatMessage('over'), 'sobre'); + }); + + test('returns key for missing messages', function () { + var localization = MQ.L10N.create('en'); + + assert.equal( + localization.formatMessage('nonexistent-key'), + 'nonexistent-key' + ); + }); + + test('formats parametric messages', function () { + var localization = MQ.L10N.create('en'); + + var result = localization.formatMessage('power-ordinal', { + number: 5 + }); + assert.equal(result, 'to the 5th power'); + }); + }); + + suite('Language Switching', function () { + test('switches from English to Spanish', function () { + var localization = MQ.L10N.create('en'); + + // Start in English + assert.equal(localization.getCurrentLanguage(), 'en'); + assert.equal(localization.formatMessage('plus'), 'plus'); + + // Switch to Spanish + localization.setLanguage('es'); + assert.equal(localization.getCurrentLanguage(), 'es'); + assert.equal(localization.formatMessage('plus'), 'más'); + }); + + test('falls back to English for unsupported languages', function () { + var localization = MQ.L10N.create('en'); + + localization.setLanguage('fr'); // French not supported + assert.equal(localization.getResolvedLanguage(), 'en'); + }); + + test('resolves language variants', function () { + var localization = MQ.L10N.create('en'); + + localization.setLanguage('en-US'); + assert.equal(localization.getResolvedLanguage(), 'en'); + + localization.setLanguage('es-MX'); + assert.equal(localization.getResolvedLanguage(), 'es'); + }); + }); + + suite('Fraction Shortcuts', function () { + test('returns fraction shortcuts in English', function () { + var localization = MQ.L10N.create('en'); + + assert.equal(localization.formatFractionShortcut(1, 2), '1 half'); + assert.equal(localization.formatFractionShortcut(1, 3), '1 third'); + assert.equal(localization.formatFractionShortcut(1, 4), '1 quarter'); + }); + + test('returns fraction shortcuts in Spanish', function () { + var localization = MQ.L10N.create('en'); + localization.setLanguage('es'); + + assert.equal(localization.formatFractionShortcut(1, 2), '1 medio'); + assert.equal(localization.formatFractionShortcut(1, 3), '1 tercio'); + assert.equal(localization.formatFractionShortcut(1, 4), '1 cuarto'); + }); + + test('returns empty string for unsupported fractions', function () { + var localization = MQ.L10N.create('en'); + + // In practice, fractions with no custom translation fall back to the more technical "Start Fraction, numerator over denominator, End Fraction" syntax. + assert.equal(localization.formatFractionShortcut(2, 19), ''); + assert.equal(localization.formatFractionShortcut(5, 13), ''); + }); + }); + + suite('Auto Operator Localization', function () { + test('localizes auto operators when available', function () { + var localization = MQ.L10N.create('en'); + + // Test sin operator (should have localization) + var result = localization.formatAutoOperator('sin'); + assert.equal(result, 'sine'); + }); + + test('falls back to original name for unlocalizable operators', function () { + var localization = MQ.L10N.create('en'); + + // Test custom operator (should not have localization) + var result = localization.formatAutoOperator('customOp'); + assert.equal(result, 'customOp'); + }); + + test('handles | syntax for user-defined operators', function () { + var localization = MQ.L10N.create('en'); + + // Test operator with | syntax (visual|spoken) + var result = localization.formatAutoOperator('myop|my custom operator'); + assert.equal(result, 'my custom operator'); + + // Test operator with | but no spoken part + var result2 = localization.formatAutoOperator('op|'); + assert.equal(result2, 'op'); // Should fall back to visual part + }); + }); + + suite('Mathspeak Templates', function () { + test('creates template with comma separators', function () { + var localization = MQ.L10N.create('en'); + + var template = localization.createMathspeakTemplate( + 'start-fraction', + 'end-fraction' + ); + assert.equal(template[0], 'Start Fraction,'); + assert.equal(template[1], ', End Fraction'); + }); + + test('creates Spanish templates', function () { + var localization = MQ.L10N.create('en'); + localization.setLanguage('es'); + + var template = localization.createMathspeakTemplate( + 'start-fraction', + 'end-fraction' + ); + assert.equal(template[0], 'Inicio Fracción,'); + assert.equal(template[1], ', Fin Fracción'); + }); + }); + + suite('Language Change Callbacks', function () { + test('triggers callbacks on language change', function () { + var localization = MQ.L10N.create('en'); + var callbackTriggered = false; + var newLanguage = ''; + + var unregister = localization.onLanguageChange(function () { + callbackTriggered = true; + newLanguage = localization.getCurrentLanguage(); + }); + + localization.setLanguage('es'); + + assert.equal(callbackTriggered, true); + assert.equal(newLanguage, 'es'); + + // Clean up + unregister(); + }); + + test('does not trigger callbacks when language stays the same', function () { + var localization = MQ.L10N.create('en'); + var callbackCount = 0; + + var unregister = localization.onLanguageChange(function () { + callbackCount++; + }); + + localization.setLanguage('en'); + localization.setLanguage('en'); + + assert.equal(callbackCount, 0); + + // Clean up + unregister(); + }); + + test('allows callback unregistration', function () { + var localization = MQ.L10N.create('en'); + var callbackTriggered = false; + + var unregister = localization.onLanguageChange(function () { + callbackTriggered = true; + }); + + unregister(); // Unregister immediately so the callback is never fired + localization.setLanguage('es'); + + assert.equal(callbackTriggered, false); + }); + }); + + suite('Static Methods', function () { + test('checks language support', function () { + assert.equal(MQ.L10N.isLanguageSupported('en'), true); + assert.equal(MQ.L10N.isLanguageSupported('es'), true); + assert.equal(MQ.L10N.isLanguageSupported('fr'), false); + }); + + test('resolves language variants', function () { + assert.equal(MQ.L10N.resolveLanguage('en-US'), 'en'); + assert.equal(MQ.L10N.resolveLanguage('es-MX'), 'es'); + assert.equal(MQ.L10N.resolveLanguage('fr'), 'en'); // fallback + }); + }); +}); diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 25591fc95..70f3647b9 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -347,13 +347,13 @@ suite('Public API', function () { mq.latex('\\frac{d}{dx}\\sqrt{x}'); assertMathSpeakEqual( mq.mathspeak(), - 'StartFraction "d" Over "d" "x" EndFraction StartRoot "x" EndRoot' + 'Start Fraction "d" over "d" "x" End Fraction Start Root "x" End Root' ); mq.latex('1+2-3\\cdot\\frac{5}{6^7}=\\left(8+9\\right)'); assertMathSpeakEqual( mq.mathspeak(), - '1 plus 2 minus 3 times StartFraction 5 Over 6 to the 7th power EndFraction equals left parenthesis 8 plus 9 right parenthesis' + '1 plus 2 minus 3 times Start Fraction 5 over 6 to the 7th power End Fraction equals left parenthesis 8 plus 9 right parenthesis' ); // Example 13 from http://www.gh-mathspeak.com/examples/quick-tutorial/index.php?verbosity=v&explicitness=2&interp=0 @@ -362,7 +362,7 @@ suite('Public API', function () { ); assertMathSpeakEqual( mq.mathspeak(), - '"d" equals StartRoot left parenthesis "x" Subscript 2 Baseline minus "x" Subscript 1 Baseline right parenthesis squared minus left parenthesis "y" Subscript 2 Baseline minus "y" Subscript 1 Baseline right parenthesis squared EndRoot' + '"d" equals Start Root left parenthesis "x" Subscript 2 Baseline minus "x" Subscript 1 Baseline right parenthesis squared minus left parenthesis "y" Subscript 2 Baseline minus "y" Subscript 1 Baseline right parenthesis squared End Root' ); mq.latex('').typedText('\\langle').keystroke('Spacebar').typedText('u,v'); // .latex() doesn't work yet for angle brackets :( @@ -374,7 +374,7 @@ suite('Public API', function () { mq.latex('\\left| x \\right| + \\left( y \\right|'); assertMathSpeakEqual( mq.mathspeak(), - 'StartAbsoluteValue "x" EndAbsoluteValue plus left parenthesis "y" right pipe' + 'Start Absolute Value "x" End Absolute Value plus left parenthesis "y" right pipe' ); }); }); @@ -1439,4 +1439,638 @@ suite('Public API', function () { ); }); }); + + suite('Localization', function () { + const $ = window.test_only_jquery; + var mq; + + setup(function () { + // Reset global language to default before each test + MQ.config({ language: 'en' }); + }); + + suite('Default Language Behavior', function () { + test('default language is English with English ARIA labels and mathspeak', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Verify default language is English + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'Default ARIA label should be English' + ); + + // Create a fraction and verify English output + mq.latex('\\frac{1}{2}'); + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'ARIA label should remain English after adding content' + ); + assert.equal( + mq.mathspeak(), + '1 half', + 'Mathspeak should be in English' + ); + + // Change language to Spanish and verify Spanish output + mq.config({ language: 'es' }); + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'ARIA label should be Spanish after language change' + ); + assert.equal( + mq.mathspeak(), + '1 medio', + 'Mathspeak should be in Spanish' + ); + }); + + test('language switching updates both ARIA labels and mathspeak', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Start with a fraction in English + mq.latex('\\frac{3}{4}'); + assert.equal(mq.getAriaLabel(), 'Math Input'); + assert.equal(mq.mathspeak(), '3 quarters'); + + // Switch to Spanish + mq.config({ language: 'es' }); + assert.equal(mq.getAriaLabel(), 'Entrada Matemática'); + assert.equal(mq.mathspeak(), '3 cuartos'); + + // Switch back to English + mq.config({ language: 'en' }); + assert.equal(mq.getAriaLabel(), 'Math Input'); + assert.equal(mq.mathspeak(), '3 quarters'); + }); + + test('complex expressions in multiple languages', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Test with squared expression + mq.latex('x^2'); + assert.equal( + mq.mathspeak(), + '"x" squared', + 'English squared should work' + ); + + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" al cuadrado', + 'Spanish squared should work' + ); + + // Test with square root + mq.latex('\\sqrt{x}'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + 'Start Root, "x" , End Root', + 'English square root should work' + ); + + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + 'Inicio Raíz, "x" , Fin Raíz', + 'Spanish square root should work' + ); + }); + }); + + suite('Constructor Language Configuration', function () { + test('creating MathField with Spanish language in constructor', function () { + // Create MathField with Spanish language + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es' + }); + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'Should initialize with Spanish ARIA label' + ); + + // Create a fraction and verify Spanish output + mq.latex('\\frac{1}{2}'); + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'ARIA label should remain Spanish' + ); + assert.equal( + mq.mathspeak(), + '1 medio', + 'Mathspeak should be in Spanish' + ); + }); + + test('constructor language setting works with various expressions', function () { + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es' + }); + + // Test fraction shortcuts with Spanish pluralization + mq.latex('\\frac{1}{3}'); + assert.equal(mq.mathspeak(), '1 tercio'); + + // Test powers in Spanish + mq.latex('x^3'); + assert.equal(mq.mathspeak(), '"x" al cubo'); + + // Test cube root in Spanish + mq.latex('\\sqrt[3]{x}'); + assert.equal( + mq.mathspeak(), + 'Inicio Raíz Cúbica, "x" , Fin Raíz Cúbica' + ); + }); + + test('global language configuration affects new instances', function () { + // Set global language to Spanish + MQ.config({ language: 'es' }); + + // Create new MathField without explicit language + mq = MQ.MathField($('').appendTo('#mock')[0]); + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'Global language setting should affect new instances' + ); + + // Verify mathspeak is also in Spanish + mq.latex('\\frac{2}{3}'); + assert.equal(mq.mathspeak(), '2 tercios'); + }); + }); + + suite('Error Handling and Fallbacks', function () { + test('bogus language code throws error and falls back to English', function () { + assert.throws(function () { + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'xxx' + }); + }, 'Should throw error for invalid language code'); + }); + + test('invalid language falls back to English behavior', function () { + // Create with valid language first, then try to set invalid + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'en' + }); + + // Try to set bogus language via config - should not throw but should fail silently + try { + mq.config({ language: 'xx' }); + } catch (e) { + // Expected to throw during validation + } + + // Create fraction and verify fallback to English + mq.latex('\\frac{1}{2}'); + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'Should fall back to English ARIA label' + ); + assert.equal( + mq.mathspeak(), + '1 half', + 'Should fall back to English mathspeak' + ); + }); + + test('language validation during configuration', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Valid language switches should work + mq.config({ language: 'es' }); + assert.equal(mq.getAriaLabel(), 'Entrada Matemática'); + + mq.config({ language: 'en' }); + assert.equal(mq.getAriaLabel(), 'Math Input'); + + // Invalid language should throw + assert.throws(function () { + mq.config({ language: 'invalid' }); + }, 'Should throw for invalid language codes'); + + // Field should still work after failed language change + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'Should maintain last valid language' + ); + }); + + test('non-string language values throw errors', function () { + assert.throws(function () { + MQ.MathField($('').appendTo('#mock')[0], { + language: 123 + }); + }, 'Should throw for numeric language'); + + assert.throws(function () { + MQ.MathField($('').appendTo('#mock')[0], { + language: null + }); + }, 'Should throw for null language'); + + assert.throws(function () { + MQ.MathField($('').appendTo('#mock')[0], { + language: undefined + }); + }, 'Should throw for undefined language'); + }); + }); + + suite('Language Fallback System', function () { + test('locale-specific language falls back to base language (en-GB → en)', function () { + // Test requesting en-GB should fall back to en + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'en-GB' + }); + + // Should work as English + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'en-GB should fall back to English' + ); + + mq.latex('\\frac{1}{2}'); + assert.equal(mq.mathspeak(), '1 half', 'Should use English mathspeak'); + }); + + test('locale-specific language falls back to base language (es-AR → es)', function () { + // Test requesting es-AR should fall back to es + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es-AR' + }); + + // Should work as Spanish + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'es-AR should fall back to Spanish' + ); + + mq.latex('\\frac{1}{2}'); + assert.equal(mq.mathspeak(), '1 medio', 'Should use Spanish mathspeak'); + }); + + test('base language falls back to locale variant when available (es → es-MX)', function () { + // Note: This test demonstrates the concept, but since es-MX currently loads the same + // messages as es, the behavior will be identical. In a real implementation with + // different es-MX messages, this would show locale-specific differences. + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es' + }); + + // Should work as Spanish (potentially with Mexican variant if available) + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'es should load successfully' + ); + + mq.latex('\\frac{1}{2}'); + assert.equal(mq.mathspeak(), '1 medio', 'Should use Spanish mathspeak'); + }); + + test('cascading fallback: fr-CA → fr → en', function () { + // Test unsupported French Canadian falls back all the way to English + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'fr-CA' + }); + + // Should fall back to English as final resort + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'fr-CA should fall back to English' + ); + + mq.latex('\\frac{1}{2}'); + assert.equal(mq.mathspeak(), '1 half', 'Should use English mathspeak'); + }); + + test('runtime language switching with fallbacks', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Start with English + mq.latex('\\frac{1}{2}'); + assert.equal(mq.getAriaLabel(), 'Math Input'); + assert.equal(mq.mathspeak(), '1 half'); + + // Switch to es-CO (should fall back to es) + mq.config({ language: 'es-CO' }); + assert.equal( + mq.getAriaLabel(), + 'Entrada Matemática', + 'es-CO should fall back to Spanish' + ); + assert.equal(mq.mathspeak(), '1 medio'); + + // Switch to en-AU (should fall back to en) + mq.config({ language: 'en-AU' }); + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'en-AU should fall back to English' + ); + assert.equal(mq.mathspeak(), '1 half'); + + // Switch to de-DE (should fall back to en) + mq.config({ language: 'de-DE' }); + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'de-DE should fall back to English' + ); + assert.equal(mq.mathspeak(), '1 half'); + }); + + test('fallback preserves mathematical functionality', function () { + // Test that fallback doesn't break mathematical expression handling + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'pt-BR' + }); // Falls back to English + + // Test various mathematical expressions + mq.latex('x^2'); + assert.equal( + mq.mathspeak(), + '"x" squared', + 'Powers should work with fallback' + ); + + mq.latex('\\sqrt{x}'); + assert.equal( + mq.mathspeak(), + 'Start Root, "x" , End Root', + 'Roots should work with fallback' + ); + + mq.latex('\\sum_{i=1}^{n} x_i'); + assert.equal( + mq.mathspeak(), + 'Start sum from "i" equals 1, to, "n" , end sum "x" Subscript, "i" , Baseline', + 'Complex expressions should work with fallback' + ); + }); + }); + + suite('Advanced Localization Features', function () { + test('mathematical notation localization', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Test summation notation + mq.latex('\\sum_{i=1}^{n} x_i'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + 'Start sum from "i" equals 1, to, "n" , end sum "x" Subscript, "i" , Baseline', + 'summation notation works in English' + ); + + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + 'Inicio suma desde "i" igual 1, hasta, "n" , fin suma "x" Subíndice, "i" , Línea Base', + 'summation notation works in Spanish' + ); + }); + + test('power expressions with ordinal numbers', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Test various power expressions + mq.latex('x^4'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" to the 4th power', + 'ordinal exponent in English' + ); + + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" a la cuarta potencia', + 'ordinal exponent in Spanish' + ); + }); + + test('directional navigation announcements', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + mq.latex('\\frac{a}{b}'); + + // Focus the field so cursor navigation works + mq.focus(); + + // Test navigation - the specific implementation may vary but language should affect announcements + var initialCursor = mq.__controller.cursor; + assert.ok(initialCursor, 'Cursor should exist for navigation testing'); + }); + + test('inequality operators localization', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Test less than in English + mq.latex('x < y'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" less than "y"', + 'less than should work in English' + ); + + // Test less than in Spanish + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" menor que "y"', + 'less than should work in Spanish' + ); + + // Test greater than in English + mq.latex('x > y'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" greater than "y"', + 'greater than should work in English' + ); + + // Test greater than in Spanish + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" mayor que "y"', + 'greater than should work in Spanish' + ); + + // Test less than or equal to in English + mq.latex('x \\le y'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" less than or equal to "y"', + 'less than or equal to should work in English' + ); + + // Test less than or equal to in Spanish + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" menor o igual que "y"', + 'less than or equal to should work in Spanish' + ); + + // Test greater than or equal to in English + mq.latex('x \\ge y'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" greater than or equal to "y"', + 'greater than or equal to should work in English' + ); + + // Test greater than or equal to in Spanish + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" mayor o igual que "y"', + 'greater than or equal to should work in Spanish' + ); + + // Test not equal to in English + mq.latex('x \\ne y'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" not equal to "y"', + 'not equal to should work in English' + ); + + // Test not equal to in Spanish + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" no igual a "y"', + 'not equal to should work in Spanish' + ); + + // Test approximately equal to in English + mq.latex('x \\approx y'); + mq.config({ language: 'en' }); + assert.equal( + mq.mathspeak(), + '"x" approximately equal to "y"', + 'approximately equal to should work in English' + ); + + // Test approximately equal to in Spanish + mq.config({ language: 'es' }); + assert.equal( + mq.mathspeak(), + '"x" aproximadamente igual a "y"', + 'approximately equal to should work in Spanish' + ); + }); + }); + + suite('Edge Cases and Robustness', function () { + test('empty field localization', function () { + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es' + }); + + // Empty field should still have localized ARIA label + assert.equal(mq.getAriaLabel(), 'Entrada Matemática'); + assert.equal( + mq.mathspeak(), + '', + 'Empty field should have empty mathspeak' + ); + + // Language switching on empty field should work + mq.config({ language: 'en' }); + assert.equal(mq.getAriaLabel(), 'Math Input'); + }); + + test('language switching preserves content', function () { + mq = MQ.MathField($('').appendTo('#mock')[0]); + + // Create complex expression + mq.latex('\\frac{x^2 + 1}{\\sqrt{y}}'); + var originalLatex = mq.latex(); + + // Switch languages multiple times + mq.config({ language: 'es' }); + mq.config({ language: 'en' }); + mq.config({ language: 'es' }); + + // Content should be preserved + assert.equal( + mq.latex(), + originalLatex, + 'LaTeX content should be preserved through language switches' + ); + }); + + test('multiple MathFields with different languages', function () { + var mq1 = MQ.MathField($('').appendTo('#mock')[0], { + language: 'en' + }); + var mq2 = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es' + }); + + mq1.latex('\\frac{1}{2}'); + mq2.latex('\\frac{1}{2}'); + + // Each field should maintain its own language + assert.equal(mq1.getAriaLabel(), 'Math Input'); + assert.equal(mq2.getAriaLabel(), 'Entrada Matemática'); + assert.equal(mq1.mathspeak(), '1 half'); + assert.equal(mq2.mathspeak(), '1 medio'); + }); + + test('localization with custom ARIA labels', function () { + mq = MQ.MathField($('').appendTo('#mock')[0], { + language: 'es' + }); + + // Set custom ARIA label + mq.setAriaLabel('Campo Personalizado'); + assert.equal(mq.getAriaLabel(), 'Campo Personalizado'); + + // Language switching should not affect custom labels + mq.config({ language: 'en' }); + assert.equal( + mq.getAriaLabel(), + 'Campo Personalizado', + 'Custom ARIA labels should be preserved' + ); + + // Resetting to empty should restore localized default + mq.setAriaLabel(''); + assert.equal( + mq.getAriaLabel(), + 'Math Input', + 'Should restore localized default after clearing custom label' + ); + }); + }); + + teardown(function () { + // Clean up any global state + MQ.config({ language: 'en' }); + }); + }); }); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 53808369b..da2567b23 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -166,7 +166,7 @@ suite('typing with auto-replaces', function () { mq.latex('\\frac{1}{9}'); assertMathspeak('1 ninth'); mq.latex('\\frac{1}{10}'); - assertMathspeak('StartFraction, 1 Over 10, EndFraction'); + assertMathspeak('Start Fraction, 1 over 10, End Fraction'); // Testing plural numeric fractions from 31/2 to 31/10 mq.latex('\\frac{31}{2}'); @@ -186,7 +186,7 @@ suite('typing with auto-replaces', function () { mq.latex('\\frac{31}{9}'); assertMathspeak('31 ninths'); mq.latex('\\frac{31}{10}'); - assertMathspeak('StartFraction, 31 Over 10, EndFraction'); + assertMathspeak('Start Fraction, 31 over 10, End Fraction'); // Fractions with negative numerators should be shortened mq.latex('\\frac{-1}{2}'); @@ -198,19 +198,19 @@ suite('typing with auto-replaces', function () { // Fractions with negative denominators should not be shortened mq.latex('\\frac{1}{-2}'); - assertMathspeak('StartFraction, 1 Over negative 2, EndFraction'); + assertMathspeak('Start Fraction, 1 over negative 2, End Fraction'); // Traditional fractions should be spoken if either numerator or denominator are not numeric mq.latex('\\frac{x}{2}'); - assertMathspeak('StartFraction, "x" Over 2, EndFraction'); + assertMathspeak('Start Fraction, "x" over 2, End Fraction'); mq.latex('\\frac{2}{x}'); - assertMathspeak('StartFraction, 2 Over "x", EndFraction'); + assertMathspeak('Start Fraction, 2 over "x", End Fraction'); // Traditional fractions should be spoken if either numerator or denominator are not whole numbers mq.latex('\\frac{1.2}{2}'); - assertMathspeak('StartFraction, 1.2 Over 2, EndFraction'); + assertMathspeak('Start Fraction, 1.2 over 2, End Fraction'); mq.latex('\\frac{4}{2.3}'); - assertMathspeak('StartFraction, 4 Over 2.3, EndFraction'); + assertMathspeak('Start Fraction, 4 over 2.3, End Fraction'); // A whole number followed by a shortened fraction should include the word "and", and other combinations should not. mq.latex('3\\frac{3}{8}'); @@ -228,7 +228,7 @@ suite('typing with auto-replaces', function () { mq.latex('\\ \\frac{1}{2}'); assertMathspeak('1 half'); mq.latex('3\\frac{3}{x}'); - assertMathspeak('3 StartFraction, 3 Over "x", EndFraction'); + assertMathspeak('3 Start Fraction, 3 over "x", End Fraction'); mq.latex('x\\frac{3}{8}'); assertMathspeak('"x" 3 eighths'); }); @@ -275,9 +275,9 @@ suite('typing with auto-replaces', function () { assertMathspeak('"x" to the 999th power'); // Values greater than 1000 have no suffix mq.latex('x^{1000}'); - assertMathspeak('"x" to the 1000 power'); + assertMathspeak('"x" to the 1000th power'); mq.latex('x^{10000000000}'); - assertMathspeak('"x" to the 10000000000 power'); + assertMathspeak('"x" to the 10000000000th power'); // Ensure negative exponents are shortened mq.latex('10^{-5}'); @@ -318,7 +318,7 @@ suite('typing with auto-replaces', function () { // letters are split and delimiters are announced for remaining commands: mq.latex('\\mathit{this\\ is\\ a\\ test}'); assertMathspeak( - 'StartItalic Font "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" EndItalic Font' + 'Start Italic Font "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" End Italic Font' ); mq.latex('\\textcolor{red}{this\\ is\\ a\\ test}'); assertMathspeak( @@ -1567,21 +1567,21 @@ suite('typing with auto-replaces', function () { assertMathspeak('tilde'); mq.typedText('~'); assertLatex('\\approx'); - assertMathspeak('approximately equal'); + assertMathspeak('approximately equal to'); mq.config({ interpretTildeAsSim: true }); mq.typedText('~'); assertLatex('\\approx\\sim'); - assertMathspeak('approximately equal tilde'); + assertMathspeak('approximately equal to tilde'); mq.typedText('~'); assertLatex('\\approx\\approx'); - assertMathspeak('approximately equal approximately equal'); + assertMathspeak('approximately equal to approximately equal to'); mq.config({ interpretTildeAsSim: false }); mq.keystroke('Backspace'); assertLatex('\\approx\\sim'); - assertMathspeak('approximately equal tilde'); + assertMathspeak('approximately equal to tilde'); mq.keystroke('Backspace'); assertLatex('\\approx'); - assertMathspeak('approximately equal'); + assertMathspeak('approximately equal to'); mq.keystroke('Backspace'); assertLatex('\\sim'); assertMathspeak('tilde'); @@ -1592,7 +1592,7 @@ suite('typing with auto-replaces', function () { mq.keystroke('Backspace'); mq.typedText('~b'); assertLatex('a\\approx b'); - assertMathspeak('"a" approximately equal "b"'); + assertMathspeak('"a" approximately equal to "b"'); // Now test that tilde is properly transformed when pasting in LaTeX. mq.latex(''); @@ -1609,7 +1609,7 @@ suite('typing with auto-replaces', function () { test('typing ≈ char directly', function () { mq.typedText('≈'); assertLatex('\\approx'); - assertMathspeak('approximately equal'); + assertMathspeak('approximately equal to'); mq.keystroke('Backspace'); assertLatex('\\sim'); assertMathspeak('tilde'); @@ -1762,7 +1762,7 @@ suite('typing with auto-replaces', function () { test('overline renders as expected', function () { mq.latex('0.3\\overline{5}'); assertLatex('0.3\\overline{5}'); - assertMathspeak('0 .3 StartOverline 5 EndOverline'); + assertMathspeak('0 .3 Start Overline 5 End Overline'); }); }); });