diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f65f114 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] + +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true + +[*.{cjs,js,json,json5,jsonc,jsonl,mjs,ts,yml}] + +trim_trailing_whitespace = true + +[.*rc] + +trim_trailing_whitespace = true + +[*.md] + +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..413936d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,66 @@ +name: Bug Report +description: Report a bug or unexpected behavior. +labels: + - bug +body: + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the bug. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: Provide a minimal code example or steps to reproduce the issue. + placeholder: | + import { capitalize } from "@rcpthongta/string-utils"; + + capitalize("hello"); // expected "Hello", got "" + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + validations: + required: true + + - type: input + id: version + attributes: + label: Package Version + description: Which version of `@rcpthongta/string-utils` are you using? + placeholder: 1.0.0 + validations: + required: true + + - type: input + id: node-version + attributes: + label: Node.js Version + description: Output of `node --version`. + placeholder: v22.0.0 + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other relevant information, logs, or screenshots. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..d58faa5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,48 @@ +name: Feature Request +description: Suggest a new utility or enhancement. +labels: + - enhancement +body: + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the feature you would like. + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Describe the problem or use case this feature would address. + validations: + required: true + + - type: textarea + id: proposed-api + attributes: + label: Proposed API + description: If applicable, show how you envision the API would look. + placeholder: | + import { truncate } from "@rcpthongta/string-utils"; + + truncate("Hello, World!", 5); // "Hello..." + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Any alternative solutions or workarounds you have considered. + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other relevant information or references. + validations: + required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..76c1193 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: Continuous Integration +on: + push: + branches: + - develop + - main + pull_request: + branches: + - main +permissions: + contents: read +jobs: + lint: + uses: ./.github/workflows/linter.yml + test: + needs: lint + uses: ./.github/workflows/test.yml diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..7f1e7cc --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,21 @@ +name: Linter +on: + workflow_call: {} +permissions: + contents: read +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - name: Install dependencies + run: npm ci + - name: Run script command + run: npm run lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..512d77d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: Release +on: + push: + tags: + - "v*" +permissions: + contents: read + id-token: write +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + registry-url: https://registry.npmjs.org + - name: Install dependencies + run: npm ci + - name: Build package + run: npm run build + - name: Publish to npm + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f933b34 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: Test +on: + workflow_call: {} +permissions: + contents: read +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - name: Install dependencies + run: npm ci + - name: Run script command + run: npm run test:coverage diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..106b3b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Dependencies directory + +node_modules/* + +# Compiled output directory + +dist/* + +# Test directory + +coverage/* + +# Text editor directory and files • Visual Studio Code + +.vscode/* +!.vscode/extensions.json +!.vscode/launch.json +!.vscode/settings.json +!.vscode/tasks.json diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..da99483 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..f27575a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run precommit diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 0000000..c9272ed --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1 @@ +npm run prepush diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..21159d8 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +engine-strict = true +fund = false +update-notifier = false diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1a1a2e2 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker", + "yoavbls.pretty-ts-errors" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..06a2d78 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "[javascript][json][json5][jsonc][jsonl][markdown][typescript][yaml]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "cSpell.allowCompoundWords": true, + "cSpell.ignoreRegExpList": ["/[\\u0E00-\\u0E7F]/g"], + "cSpell.language": "en,en-US", + "cSpell.words": [ + "rcpthongta", + "uncapitalize" + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.formatOnSave": true, + "eslint.format.enable": true, + "eslint.validate": [ + "javascript", + "json", + "json5", + "jsonc", + "jsonl", + "markdown", + "typescript", + "yaml" + ], + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cf034da --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/) +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [1.0.0] - 2026-09-23 + +### Added + +- **String Inspection** + - `hasText` — check if a string contains non-whitespace characters + - `hasLength` — check if a string contains one or more characters + - `defaultIfBlank` — return a fallback when a string is blank + - `defaultIfEmpty` — return a fallback when a string is empty + +- **Case Conversion** + - `capitalize` — uppercase the first character + - `uncapitalize` — lowercase the first character + - `camelCase` — convert a string to camelCase + - `pascalCase` — convert a string to PascalCase + - `kebabCase` — convert a string to kebab-case + - `snakeCase` — convert a string to snake_case + - `constantCase` — convert a string to CONSTANT_CASE + +- **String Transformation** + - `collapseWhitespace` — normalize consecutive whitespace to single spaces + - `slugify` — convert a string to a URL-friendly slug + - `format` — format strings using templates and placeholders + - `mask` — mask portions of a string using a pattern + - `redact` — redact sensitive string content + +- **Constants & Patterns** + - Reusable string constants such as `EMPTY`, `SPACE`, `HYPHEN`, `UNDERSCORE`, `DOT`, `ASTERISK`, and `HASH` + - Unicode-aware regular expression patterns for words, digits, letters, and diacritics + +- **Package Distribution** + - ESM, CJS, and IIFE builds powered by tsup + - TypeScript type declarations + - Dual `import` / `require` package exports + +- **Development & CI** + - Vitest for unit tests, benchmarks, and coverage + - ESLint and Prettier for code quality and formatting + - Husky, lint-staged, and commitlint for Git hooks and commit validation + - GitHub Actions workflows for linting and test coverage + +[1.0.0]: https://github.com/rcpthongta/string-utils/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c83dfe4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,169 @@ +# Contributing + +Thank you for considering contributing to `@rcpthongta/string-utils`! +This document outlines the development workflow, coding standards, and guidelines for submitting contributions. + +## Prerequisites + +- [Node.js](https://nodejs.org/) >= 22 +- [npm](https://www.npmjs.com/) + +## Getting Started + +1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the repository and clone your fork. +2. Create a new branch from `develop`. +3. Install dependencies: + +```bash +npm install +``` + +## Project Structure + +```text +src/ +├── constants/ # Shared string constants and regex patterns +├── types/ # Shared TypeScript type definitions +├── libs/ +│ └── / +│ ├── .lib.ts # Implementation +│ ├── .lib.spec.ts # Unit tests +│ ├── .lib.bench.ts # Benchmarks +│ └── index.ts # Public export +└── index.ts # Package entry point +``` + +## Development Workflow + +### Branching + +All work should be done in a feature branch created from `develop` in your fork. +Please use the following branch naming conventions: + +- `feat/` +- `fix/` +- `chore/` +- `docs/` +- `perf/` + +> **Note:** Do not push branches directly to the upstream repository. + +### Commit Messages + +This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification, enforced by commitlint. +All commit messages must use the following format: + +```text +(): +``` + +**Allowed types:** + +`feat` `fix` `docs` `style` `refactor` `perf` `test` `build` `ci` `chore` `revert` `merge` + +**Examples:** + +```text +feat(capitalize): add locale-aware capitalization option +test(slugify): add edge case tests for unicode input +fix(mask): handle empty string input +``` + +### Available Scripts + +| Command | Description | +| ----------------------- | ----------------------------------------------------- | +| `npm run build` | Clean and build ESM, CJS, IIFE, and type declarations | +| `npm run test` | Run all unit tests | +| `npm run test:watch` | Run tests in watch mode | +| `npm run test:coverage` | Run tests with coverage report | +| `npm run bench` | Run benchmarks | +| `npm run lint` | Check for lint errors | +| `npm run lint:fix` | Auto-fix lint errors | + +### Git Hooks + +Git hooks are managed by [Husky](https://typicode.github.io/husky/) and run automatically: + +- **pre-commit** — runs lint-staged to auto-fix staged files via ESLint +- **commit-msg** — validates the commit message format via commitlint +- **pre-push** — runs the full test suite and build to prevent broken pushes + +## Development Standards + +All contributions must follow the project's coding, linting, testing, and benchmarking standards. + +Before submitting a pull request, please review the following guidelines: + +- **Core Principles & Coding Standards** — Performance, memory usage, TypeScript typing, architecture, null safety, function design, and JSDoc requirements. +- **Linting & Code Quality Standards** — Naming conventions, explicit type definitions, unused code, regular expressions, and complexity limits. +- **Testing & Benchmarking Standards** — Vitest test organization, coverage requirements, test case design, and benchmark structure. + +These standards apply to both new utilities and changes to existing utilities. + +## Adding a New Utility + +1. Create a new directory under `src/libs//`. +2. Add the following files: + +```text +.lib.ts # Implementation +.lib.spec.ts # Unit tests +.lib.bench.ts # Benchmarks +index.ts # Re-export the public API +``` + +3. Register the new utility by exporting it from: + +```text +src/libs/index.ts +src/index.ts +``` + +4. Verify that all tests pass and coverage remains at **100%**. + +## Testing + +All contributions must include unit tests. +This project enforces **100%** code coverage thresholds across: + +- Statements +- Branches +- Functions +- Lines + +To verify coverage locally: + +```bash +npm run test:coverage +``` + +## Submitting a Pull Request + +Before opening a pull request, please ensure the following: + +1. Your branch is up to date with the upstream `develop` branch. +2. All linting, tests, and builds pass locally: + +```bash +npm run lint +npm run test +npm run build +``` + +3. Push your branch to your fork: + +```bash +git push origin +``` + +4. Open a pull request from your fork targeting the upstream `develop` branch. +5. Write a clear and concise description of the changes and their motivation. +6. Keep each pull request focused on a single concern. +7. Respond to review feedback promptly. + +All CI checks must pass before a pull request can be merged. + +## License + +By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE.md). diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..11da323 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 rcpthongta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2f9095c..1c1fe5e 100644 --- a/README.md +++ b/README.md @@ -1 +1,134 @@ -# String Utilities +# @rcpthongta/string-utils + +A lightweight, type-safe, dependency-free string utility library for TypeScript and modern JavaScript applications. + +--- + +## Features + +- 🪶 **Zero Dependencies**: Pure vanilla TypeScript with zero third-party runtime dependencies. +- 🌲 **Tree-Shakeable**: Side-effect free, allowing bundlers to include only the functions you actually use. +- 🛡️ **Type-Safe**: Written in strict TypeScript with comprehensive type definitions. +- 📦 **Universal Modules**: Supports ESM, CommonJS, and browser global (IIFE) via CDN. +- ⚡ **High Performance**: Optimized implementations benchmarked and thoroughly tested. + +--- + +## Documentation + +> 📖 **Interactive Documentation & API Playground** +> +> Full API references, interactive examples, and detailed guides are available on our official website: +> +> 👉 **[Visit Documentation Website](https://github.com/rcpthongta/string-utils#readme)** *(Link to docs site)* + +--- + +## Installation + +Install using your favorite package manager: + +```bash +# npm +npm install @rcpthongta/string-utils + +# pnpm +pnpm add @rcpthongta/string-utils + +# yarn +yarn add @rcpthongta/string-utils + +# bun +bun add @rcpthongta/string-utils +``` + +### CDN / Direct Browser Usage + +For quick prototyping or environments without a build step: + +```html + + + + + + + +``` + +--- + +## Quick Start + +Import only the utilities you need: + +```typescript +import { + camelCase, + capitalize, + defaultIfBlank, + mask, + redact, + slugify +} from "@rcpthongta/string-utils"; + +// Case transformation +capitalize("hello world"); // "Hello world" +camelCase("user_profile_id"); // "userProfileId" + +// Sanitization & URL +slugify("Hello World & Special Characters!"); // "hello-world-and-special-characters" + +// Validation & Fallback +defaultIfBlank(" ", "default value"); // "default value" + +// Formatting with mask pattern +mask("0812345678", "(###) ###-####"); // "(081) 234-5678" +mask("1234567890123456", "#### #### #### ####"); // "1234 5678 9012 3456" + +// Privacy & Censoring +redact("0812345678", { visibleStart: 3, visibleEnd: 2 }); // "081*****78" +``` + +CommonJS is also fully supported: + +```javascript +const { capitalize, slugify } = require("@rcpthongta/string-utils"); +``` + +--- + +## Overview of Available Utilities + +Below is a summary of the utility functions available in this package: + +| Category | Functions | +|-----------------------------|---------------------------------------------------------------------| +| **Case Conversion** | `camelCase`, `constantCase`, `kebabCase`, `pascalCase`, `snakeCase` | +| **Casing & Capitalization** | `capitalize`, `uncapitalize` | +| **Formatting & Templating** | `collapseWhitespace`, `format`, `createFormatter`, `slugify` | +| **Pattern Masking** | `mask`, `createMask` | +| **Validation & Fallback** | `hasLength`, `hasText`, `defaultIfBlank`, `defaultIfEmpty` | +| **Privacy & Redaction** | `redact` | + +*(Refer to the [Documentation Website](https://github.com/rcpthongta/string-utils#readme) for full parameter signatures, options, and live demos).* + +--- + +## Contributing + +Contributions are always welcome! Please check out our [Contributing Guide](CONTRIBUTING.md) before submitting a Pull Request. + +--- + +## Security + +If you discover any security-related issues, please review our [Security Policy](SECURITY.md). + +--- + +## License + +This project is licensed under the [MIT License](LICENSE.md) © [rcpthongta](https://github.com/rcpthongta). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..97fff1f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,62 @@ +# Security Policy + +## Supported Versions + +Security updates are currently provided for the latest published version of `@rcpthongta/string-utils`. + +| Version | Supported | +| -------------- | ------------------ | +| Latest | :white_check_mark: | +| Older versions | :x: | + +## Reporting a Vulnerability + +If you discover a security vulnerability in `@rcpthongta/string-utils`, please report it privately. + +Please do not report security vulnerabilities through public GitHub issues. + +To report a vulnerability, please use [GitHub Private Vulnerability Reporting](https://github.com/rcpthongta/string-utils/security/advisories/new). + +When reporting a vulnerability, please include: + +- A clear description of the vulnerability +- The affected version +- Steps to reproduce the issue +- A minimal proof of concept, if applicable +- Any potential impact or attack scenario + +You may redact or omit sensitive information that is not necessary to reproduce the issue. + +## Response Process + +After receiving a vulnerability report, we will: + +1. Review and validate the report. +2. Assess the severity and potential impact. +3. Work on a fix or mitigation when necessary. +4. Release a patched version when appropriate. +5. Credit the reporter, with their permission. + +We aim to handle security reports responsibly and keep affected users informed when a security issue is confirmed. + +## Scope + +This security policy applies to: + +- The source code of `@rcpthongta/string-utils` +- Published packages distributed through npm +- Public APIs and functionality provided by the package + +Issues that are not security vulnerabilities, including general bugs, feature requests, documentation issues, and usage questions, should be reported through the project's regular [GitHub issue tracker](https://github.com/rcpthongta/string-utils/issues). + +## Disclosure + +Please allow reasonable time for investigation and remediation before publicly disclosing a confirmed security vulnerability. + +Once a security issue has been addressed, relevant details may be disclosed publicly through the project's changelog or release notes when appropriate. + +## Security Updates + +When a security vulnerability is confirmed and requires a code change, a patched version will be released when appropriate. + +Users are encouraged to keep `@rcpthongta/string-utils` updated to the latest published version to receive available security fixes and improvements. diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 0000000..48c34d5 --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,10 @@ +import conventionalConfig from "@commitlint/config-conventional"; + +const configuration = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [2, "always", [...conventionalConfig.rules["type-enum"][2], "merge"]] + } +}; + +export default configuration; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..cf3114f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,183 @@ +import javascript from "@eslint/js"; +import markdown from "@eslint/markdown"; + +import jsonc from "eslint-plugin-jsonc"; +import prettier from "eslint-plugin-prettier/recommended"; +import promise from "eslint-plugin-promise"; +import sonarjs from "eslint-plugin-sonarjs"; +import unusedImports from "eslint-plugin-unused-imports"; +import yml from "eslint-plugin-yml"; +import eslint from "eslint/config"; +import globals from "globals"; +import jsoncParser from "jsonc-eslint-parser"; +import typescript from "typescript-eslint"; +import yamlParser from "yaml-eslint-parser"; + +const jsFiles = ["**/*.cjs", "**/*.js", "**/*.mjs"]; +const tsFiles = ["**/*.ts"]; +const sourceFiles = [...jsFiles, ...tsFiles]; + +const configuration = eslint.defineConfig( + eslint.globalIgnores(["**/coverage/*", "**/dist/*", "**/node_modules/*", "**/.npmrc", "**/package-lock.json"]), + { + files: sourceFiles, + languageOptions: { + globals: { + ...globals.browser, + ...globals.es2022, + ...globals.nodeBuiltin + } + } + }, + { + ...prettier, + files: sourceFiles + }, + { + files: sourceFiles, + plugins: { + "unused-imports": unusedImports + }, + rules: { + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { + args: "after-used", + argsIgnorePattern: "^_", + vars: "all", + varsIgnorePattern: "^_" + } + ] + } + }, + { + files: sourceFiles, + plugins: { + promise + }, + rules: { + ...promise.configs["flat/recommended"].rules, + "promise/always-return": [ + "error", + { + ignoreLastCallback: true + } + ], + "promise/no-callback-in-promise": "error", + "promise/no-multiple-resolved": "error", + "promise/no-nesting": "error", + "promise/no-promise-in-callback": "error", + "promise/no-return-in-finally": "error", + "promise/valid-params": "error" + } + }, + { + files: sourceFiles, + extends: [sonarjs.configs.recommended], + rules: { + "sonarjs/no-duplicate-string": "off", + "sonarjs/regex-complexity": [ + "error", + { + threshold: 25 + } + ] + } + }, + { + files: ["**/*.bench.ts", "**/*.benchmark.ts"], + rules: { + "sonarjs/assertions-in-tests": "off" + } + }, + { + files: jsFiles, + extends: [javascript.configs.recommended], + rules: { + "no-unused-vars": "off" + } + }, + { + files: tsFiles, + plugins: { + "@typescript-eslint": typescript.plugin + }, + extends: [typescript.configs.recommended], + languageOptions: { + parser: typescript.parser, + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + sourceType: "module" + } + }, + rules: { + "@typescript-eslint/no-unused-vars": "off" + } + }, + ...jsonc.configs["flat/recommended-with-jsonc"].map((config) => ({ + ...config, + files: ["**/*.*rc", "**/*.json", "**/*.json5", "**/*.jsonc", "**/*.jsonl"], + languageOptions: { + ...config.languageOptions, + parser: jsoncParser, + parserOptions: { + jsonSyntax: "JSONC" + } + }, + rules: { + ...config.rules, + "jsonc/array-bracket-newline": "error", + "jsonc/array-bracket-spacing": "error", + "jsonc/array-element-newline": "error", + "jsonc/comma-dangle": "error", + "jsonc/comma-style": "error", + "jsonc/indent": ["error", 2], + "jsonc/key-spacing": "error", + "jsonc/no-irregular-whitespace": "error", + "jsonc/no-octal-escape": "error", + "jsonc/object-curly-newline": "error", + "jsonc/object-curly-spacing": "error", + "jsonc/object-property-newline": "error" + } + })), + ...yml.configs["flat/recommended"].map((config) => ({ + ...config, + files: ["**/*.yml", "**/*.yaml"], + languageOptions: { + ...config.languageOptions, + parser: yamlParser, + parserOptions: { + defaultYAMLVersion: "1.2" + } + }, + rules: { + ...config.rules, + "yml/block-mapping": "error", + "yml/block-mapping-question-indicator-newline": "error", + "yml/block-sequence-hyphen-indicator-newline": "error", + "yml/block-sequence": "error", + "yml/file-extension": [ + "error", + { + extension: "yml" + } + ], + "yml/flow-mapping-curly-newline": "error", + "yml/flow-mapping-curly-spacing": "error", + "yml/flow-sequence-bracket-newline": "error", + "yml/flow-sequence-bracket-spacing": "error", + "yml/indent": "error", + "yml/key-spacing": "error", + "yml/no-multiple-empty-lines": "error", + "yml/no-trailing-zeros": "error", + "yml/plain-scalar": "error", + "yml/quotes": "error", + "yml/spaced-comment": "error" + } + })), + ...markdown.configs.recommended +); + +export default configuration; diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs new file mode 100644 index 0000000..5a9f317 --- /dev/null +++ b/lint-staged.config.mjs @@ -0,0 +1,5 @@ +const configuration = { + "**/*.{*rc,cjs,js,json,json5,jsonc,jsonl,md,mjs,ts,yaml,yml}": ["eslint --fix"] +}; + +export default configuration; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a387e10 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6787 @@ +{ + "name": "@rcpthongta/string-utils", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@rcpthongta/string-utils", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@commitlint/cli": "^21.2.2", + "@commitlint/config-conventional": "^21.2.2", + "@eslint/js": "^9.39.5", + "@eslint/markdown": "^7.5.1", + "@types/node": "^24.13.5", + "@vitest/coverage-v8": "^5.0.1", + "eslint": "^9.39.5", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsonc": "^2.21.1", + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-promise": "^7.3.0", + "eslint-plugin-sonarjs": "^4.2.0", + "eslint-plugin-unused-imports": "^4.4.1", + "eslint-plugin-yml": "^1.19.1", + "globals": "^17.12.0", + "husky": "^9.1.7", + "jsonc-eslint-parser": "^2.4.2", + "lint-staged": "^17.5.1", + "rimraf": "^6.1.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.65.0", + "vitest": "^5.0.1", + "yaml-eslint-parser": "^1.3.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/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/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@commitlint/cli": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.2.2.tgz", + "integrity": "sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-conventional": "^21.2.2", + "@commitlint/format": "^21.2.2", + "@commitlint/lint": "^21.2.2", + "@commitlint/load": "^21.2.2", + "@commitlint/read": "^21.2.1", + "@commitlint/types": "^21.2.0", + "tinyexec": "^1.0.0", + "yargs": "^18.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/cli/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.2.2.tgz", + "integrity": "sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.2.0", + "conventional-changelog-conventionalcommits": "^10.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.2.0.tgz", + "integrity": "sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.2.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/config-validator/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/@commitlint/config-validator/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/@commitlint/ensure": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.2.0.tgz", + "integrity": "sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.2.0", + "es-toolkit": "^1.46.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", + "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/format": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.2.2.tgz", + "integrity": "sha512-v6fvxZSc/AvVMROlr3H34+1766bZSYApRUSCAMjWamStPjKMvZ8GdvVA5YW/VQNgbFTmcMz6OYmSTJEvIjPrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.2.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.2.2.tgz", + "integrity": "sha512-9UoKNgfFE3LU7FrzierCvk3CdDfMDeVGC86qZiT/n0TIjfq/dmZ9MHuXd45OTNRa26ZanmJRxEtmiXk/lEJihg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.2.0", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/lint": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.2.2.tgz", + "integrity": "sha512-Fy8JxEBzdmsYWFude/61GxXu5O+wEymwiRK2z9GL9R8mCsXphCoGxAFc5iHn5mjlfcSrhiiONE+ksf4KOjnaPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^21.2.2", + "@commitlint/parse": "^21.2.2", + "@commitlint/rules": "^21.2.2", + "@commitlint/types": "^21.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/load": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.2.2.tgz", + "integrity": "sha512-0Tt6wDPX167cjKC5D4zhm0+20wJJG+TN/TKovMOspfSe78rOnKX+MNzlVNiu6HyQPZChPJ8QBH31MVt6Bb8fCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^21.2.0", + "@commitlint/execute-rule": "^21.0.1", + "@commitlint/resolve-extends": "^21.2.2", + "@commitlint/types": "^21.2.0", + "cosmiconfig": "^9.0.1", + "cosmiconfig-typescript-loader": "^6.1.0", + "es-toolkit": "^1.46.0", + "is-plain-obj": "^4.1.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/message": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.2.0.tgz", + "integrity": "sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/parse": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.2.2.tgz", + "integrity": "sha512-MEkobPfvRp+z06Wro8HMG1BDGHzZmj82A1LH1nWeG3ipHpg/x4m6v3wEDvMBIKjRFUnfR3nBeFs3MVCr7UdAmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^21.2.0", + "conventional-changelog-angular": "^9.0.0", + "conventional-commits-parser": "^7.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/read": { + "version": "21.2.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.2.1.tgz", + "integrity": "sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^21.2.0", + "@commitlint/types": "^21.2.0", + "@conventional-changelog/git-client": "^3.0.0", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/read/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.2.2.tgz", + "integrity": "sha512-RPkJ/IFi7sMUUVbZLqwWFtWw/zRDcfFsmrPSiTMrt5wb7AdxOr86EGQFvmGzef5QKV5IPBWWCujqVTw1RWX44A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^21.2.0", + "@commitlint/types": "^21.2.0", + "es-toolkit": "^1.46.0", + "global-directory": "^5.0.0", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/resolve-extends/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/@commitlint/rules": { + "version": "21.2.2", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.2.2.tgz", + "integrity": "sha512-eplQzyYkBjYB1HyyRj8hkcK11Y9DU9nuBz7uOKEd6NpE9NGDytLFCAnlRE+OoiK/5sHEJsaz2RGhuWBvYzIbNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^21.2.0", + "@commitlint/message": "^21.2.0", + "@commitlint/to-lines": "^21.0.1", + "@commitlint/types": "^21.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-21.0.1.tgz", + "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/top-level": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.2.0.tgz", + "integrity": "sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@commitlint/types": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.2.0.tgz", + "integrity": "sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-parser": "^7.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@conventional-changelog/git-client": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-3.1.2.tgz", + "integrity": "sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/child-process-utils": "^2.0.0", + "@simple-libs/stream-utils": "^2.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "conventional-commits-filter": "^6.0.1", + "conventional-commits-parser": "^7.1.2" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/@conventional-changelog/template": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/template/-/template-1.4.0.tgz", + "integrity": "sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/markdown": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@eslint/markdown/-/markdown-7.5.1.tgz", + "integrity": "sha512-R8uZemG9dKTbru/DQRPblbJyXpObwKzo8rv1KYGGuPUPtjM4LXBYM9q5CIZAComzZupws3tWbDwam5AFpPLyJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@eslint/core": "^0.17.0", + "@eslint/plugin-kit": "^0.4.1", + "github-slugger": "^2.0.0", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm": "^3.0.0", + "micromark-util-normalize-identifier": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "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/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", + "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", + "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", + "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", + "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", + "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", + "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", + "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", + "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", + "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", + "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", + "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", + "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", + "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", + "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", + "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz", + "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz", + "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz", + "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz", + "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz", + "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz", + "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz", + "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz", + "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz", + "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz", + "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz", + "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz", + "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz", + "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz", + "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz", + "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz", + "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz", + "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz", + "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz", + "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz", + "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz", + "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz", + "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz", + "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz", + "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz", + "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@simple-libs/child-process-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-2.0.0.tgz", + "integrity": "sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@simple-libs/stream-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-2.0.0.tgz", + "integrity": "sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@sonarsource/analyzer-commons-configurations": { + "version": "2.31.0-5284", + "resolved": "https://registry.npmjs.org/@sonarsource/analyzer-commons-configurations/-/analyzer-commons-configurations-2.31.0-5284.tgz", + "integrity": "sha512-ruk3blMs9BE9RNSp9LPpWZAIjxJmq4Cp00E3cELIHz7Kym3WxdWGRlKOMYLRQ5tJg/de7jN5AtEElY3bGIfTcw==", + "dev": true, + "license": "LGPL-3.0-only" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "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/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.5.tgz", + "integrity": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.1.tgz", + "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/istanbul-lib-coverage": "^1.0.0", + "@vitest/istanbul-lib-report": "^1.0.0", + "ast-v8-to-istanbul": "^1.0.5", + "magicast": "^0.5.4", + "obug": "^2.1.4", + "std-env": "^4.2.0", + "tinyrainbow": "^3.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "5.0.1", + "vitest": "5.0.1" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/istanbul-lib-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz", + "integrity": "sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/istanbul-lib-report": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-1.0.1.tgz", + "integrity": "sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/istanbul-lib-coverage": "1.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", + "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.1", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", + "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/argue-cli": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/argue-cli/-/argue-cli-3.2.0.tgz", + "integrity": "sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.6.tgz", + "integrity": "sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-9.4.0.tgz", + "integrity": "sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@conventional-changelog/template": "^1.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-10.4.0.tgz", + "integrity": "sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@conventional-changelog/template": "^1.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/conventional-commits-parser": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz", + "integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^2.0.0", + "argue-cli": "^3.1.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff-sequences": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "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" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.5.tgz", + "integrity": "sha512-vAUHYzue4YAa2hNACjB8HvUQj5yehAZgiClyFVVom9cP8z5NSFq3PwB/TtJslN2zAMgRX6FCFCjYBbQh71g5RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-json-compat-utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.3.tgz", + "integrity": "sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esquery": "^1.6.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": "*", + "jsonc-eslint-parser": "^2.4.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@eslint/json": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsonc": { + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.21.1.tgz", + "integrity": "sha512-dbNR5iEnQeORwsK2WZzr3QaMtFCY3kKJVMRHPzUpKzMhmVy2zIpVgFDpX8MNoIdoqz6KCpCfOJavhfiSbZbN+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.5.1", + "diff-sequences": "^27.5.1", + "eslint-compat-utils": "^0.6.4", + "eslint-json-compat-utils": "^0.2.1", + "espree": "^9.6.1 || ^10.3.0", + "graphemer": "^1.4.0", + "jsonc-eslint-parser": "^2.4.0", + "natural-compare": "^1.4.0", + "synckit": "^0.6.2 || ^0.7.3 || ^0.11.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-promise": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz", + "integrity": "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-sonarjs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-4.2.1.tgz", + "integrity": "sha512-WGsnVkUxmHyBOytShcIeE+vhQaS8e+gYgbHNNi4y2e5Zj+DpE2DLTvarqDUhNwjceTZHGLgevSgfBSEVkt4EaA==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@sonarsource/analyzer-commons-configurations": "^2.31.0-5284", + "builtin-modules": "^3.3.0", + "bytes": "^3.1.2", + "functional-red-black-tree": "^1.0.1", + "globals": "^17.12.0", + "jsx-ast-utils-x": "^0.1.0", + "lodash.merge": "^4.6.2", + "minimatch": "^10.2.6", + "scslre": "^0.3.0", + "semver": "^7.8.5", + "ts-api-utils": "^2.5.0", + "typescript": ">=5 <6.1.0", + "yaml": "^2.9.0" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-yml": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-1.19.1.tgz", + "integrity": "sha512-bYkOxyEiXh9WxUhVYPELdSHxGG5pOjCSeJOVkfdIyj6tuiHDxrES2WAW1dBxn3iaZQey57XflwLtCYRcNPOiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.2", + "diff-sequences": "^27.5.1", + "escape-string-regexp": "4.0.0", + "eslint-compat-utils": "^0.6.0", + "natural-compare": "^1.4.0", + "yaml-eslint-parser": "^1.2.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-directory": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "6.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "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": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-eslint-parser": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.2.tgz", + "integrity": "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/jsonc-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/jsonc-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/jsx-ast-utils-x": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils-x/-/jsx-ast-utils-x-0.1.0.tgz", + "integrity": "sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.5.1.tgz", + "integrity": "sha512-7EDuco1xnBMeVpvAbeMq1U5KXwJGLmY8q6l+Ye78r36C4mPc+Vg1Z2SK8gHyRTyvPro90A0q5/FAZ+Au23d6QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.7", + "string-argv": "^0.3.2", + "tinyexec": "^1.3.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/lint-staged/node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "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": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", + "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.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-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rolldown": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", + "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.150.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.9", + "@rolldown/binding-android-arm64": "1.2.9", + "@rolldown/binding-darwin-arm64": "1.2.9", + "@rolldown/binding-darwin-x64": "1.2.9", + "@rolldown/binding-freebsd-x64": "1.2.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", + "@rolldown/binding-linux-arm64-gnu": "1.2.9", + "@rolldown/binding-linux-arm64-musl": "1.2.9", + "@rolldown/binding-linux-ppc64-gnu": "1.2.9", + "@rolldown/binding-linux-s390x-gnu": "1.2.9", + "@rolldown/binding-linux-x64-gnu": "1.2.9", + "@rolldown/binding-linux-x64-musl": "1.2.9", + "@rolldown/binding-openharmony-arm64": "1.2.9", + "@rolldown/binding-win32-arm64-msvc": "1.2.9", + "@rolldown/binding-win32-x64-msvc": "1.2.9" + } + }, + "node_modules/rollup": { + "version": "4.63.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz", + "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.3", + "@rollup/rollup-android-arm64": "4.63.3", + "@rollup/rollup-darwin-arm64": "4.63.3", + "@rollup/rollup-darwin-x64": "4.63.3", + "@rollup/rollup-freebsd-arm64": "4.63.3", + "@rollup/rollup-freebsd-x64": "4.63.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", + "@rollup/rollup-linux-arm-musleabihf": "4.63.3", + "@rollup/rollup-linux-arm64-gnu": "4.63.3", + "@rollup/rollup-linux-arm64-musl": "4.63.3", + "@rollup/rollup-linux-loong64-gnu": "4.63.3", + "@rollup/rollup-linux-loong64-musl": "4.63.3", + "@rollup/rollup-linux-ppc64-gnu": "4.63.3", + "@rollup/rollup-linux-ppc64-musl": "4.63.3", + "@rollup/rollup-linux-riscv64-gnu": "4.63.3", + "@rollup/rollup-linux-riscv64-musl": "4.63.3", + "@rollup/rollup-linux-s390x-gnu": "4.63.3", + "@rollup/rollup-linux-x64-gnu": "4.63.3", + "@rollup/rollup-linux-x64-musl": "4.63.3", + "@rollup/rollup-openbsd-x64": "4.63.3", + "@rollup/rollup-openharmony-arm64": "4.63.3", + "@rollup/rollup-win32-arm64-msvc": "4.63.3", + "@rollup/rollup-win32-ia32-msvc": "4.63.3", + "@rollup/rollup-win32-x64-gnu": "4.63.3", + "@rollup/rollup-win32-x64-msvc": "4.63.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/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/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", + "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.1", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-preview": "5.0.1", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.1", + "@vitest/coverage-v8": "5.0.1", + "@vitest/ui": "5.0.1", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-eslint-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.3.2.tgz", + "integrity": "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/yaml-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..af3611c --- /dev/null +++ b/package.json @@ -0,0 +1,82 @@ +{ + "version": "1.0.0", + "name": "@rcpthongta/string-utils", + "description": "A lightweight, type-safe, dependency-free string utility library for TypeScript and modern JavaScript applications.", + "author": "rcpthongta", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/rcpthongta/string-utils.git" + }, + "bugs": { + "url": "https://github.com/rcpthongta/string-utils/issues" + }, + "keywords": [ + "string", + "string-utils", + "string-library", + "string-helpers" + ], + "type": "module", + "sideEffects": false, + "main": "./dist/cjs/index.cjs", + "module": "./dist/esm/index.mjs", + "unpkg": "./dist/iife/index.global.js", + "jsdelivr": "./dist/iife/index.global.js", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.mjs", + "require": "./dist/cjs/index.cjs" + } + }, + "files": [ + "dist", + "CHANGELOG.md" + ], + "engines": { + "node": ">=22" + }, + "scripts": { + "install:husky": "husky", + "precommit": "lint-staged", + "prepush": "npm run test && npm run build", + "build": "npm run build:clean && npm run build:js && npm run build:types", + "build:clean": "rimraf dist", + "build:js": "tsup", + "build:types": "tsc -p tsconfig.types.json", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "bench": "vitest bench --run", + "lint": "eslint **/*.{*rc,cjs,js,json,json5,jsonc,jsonl,md,mjs,ts,yaml,yml}", + "lint:fix": "eslint --fix **/*.{*rc,cjs,js,json,json5,jsonc,jsonl,md,mjs,ts,yaml,yml}" + }, + "devDependencies": { + "@commitlint/cli": "^21.2.2", + "@commitlint/config-conventional": "^21.2.2", + "@eslint/js": "^9.39.5", + "@eslint/markdown": "^7.5.1", + "@types/node": "^24.13.5", + "@vitest/coverage-v8": "^5.0.1", + "eslint": "^9.39.5", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jsonc": "^2.21.1", + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-promise": "^7.3.0", + "eslint-plugin-sonarjs": "^4.2.0", + "eslint-plugin-unused-imports": "^4.4.1", + "eslint-plugin-yml": "^1.19.1", + "globals": "^17.12.0", + "husky": "^9.1.7", + "jsonc-eslint-parser": "^2.4.2", + "lint-staged": "^17.5.1", + "rimraf": "^6.1.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.65.0", + "vitest": "^5.0.1", + "yaml-eslint-parser": "^1.3.2" + } +} diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000..26ccbf7 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,15 @@ +const configuration = { + arrowParens: "always", + bracketSpacing: true, + embeddedLanguageFormatting: "auto", + endOfLine: "lf", + printWidth: 120, + quoteProps: "consistent", + semi: true, + singleQuote: false, + tabWidth: 2, + trailingComma: "none", + useTabs: false +}; + +export default configuration; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..b1b8fa4 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,2 @@ +export * from "./regex.constant"; +export * from "./string-values.constant"; diff --git a/src/constants/regex.constant.ts b/src/constants/regex.constant.ts new file mode 100644 index 0000000..fcade8d --- /dev/null +++ b/src/constants/regex.constant.ts @@ -0,0 +1,49 @@ +/** + * Matches one or more consecutive whitespace characters. + */ +export const CONSECUTIVE_WHITESPACE_REGEX: RegExp = /\s+/g; + +/** + * Matches Unicode combining diacritical marks in the U+0300–U+036F range. + */ +export const DIACRITICS_REGEX: RegExp = /[\u0300-\u036f]/g; + +/** + * Matches indexed format placeholders with optional dot-separated property paths. + * + * Captures the full path inside the braces (e.g., `"0"`, `"0.name"`, `"1.items.0.label"`). + */ +export const FORMAT_PLACEHOLDER_REGEX: RegExp = /\{(\d+(?:\.\w+)*)\}/g; + +/** + * Matches one or more consecutive characters that are not Unicode letters, combining marks, or numbers. + */ +export const NON_ALPHANUMERIC_REGEX: RegExp = /[^\p{L}\p{M}\p{N}]+/gu; + +/** + * Matches a Unicode decimal digit character, including digits from non-ASCII scripts. + */ +export const UNICODE_DIGIT_REGEX: RegExp = /\p{Nd}/u; + +/** + * Matches a Unicode letter character or combining mark. + */ +export const UNICODE_LETTER_REGEX: RegExp = /[\p{L}\p{M}]/u; + +/** + * Matches strings that contain whitespace requiring normalization. + * + * This includes non-space whitespace characters, consecutive spaces, leading spaces, and trailing spaces. + */ +export const WHITESPACE_TEST_REGEX: RegExp = /[^\S ]| {2,}|^ | $/; + +/** + * Matches individual words, acronyms, and Unicode number sequences. + * + * The pattern recognizes: + * - Cased and non-cased words, including letters with combining marks. + * - Acronyms consisting of consecutive uppercase letters. + * - Unicode number sequences. + */ +export const WORD_REGEX: RegExp = + /(?:\p{Lu}\p{M}*)?[\p{Ll}\p{Lo}][\p{Ll}\p{Lo}\p{M}]*|\p{Lu}[\p{Lu}\p{M}]*(?![\p{Ll}\p{Lo}])|\p{N}+/gu; diff --git a/src/constants/string-values.constant.ts b/src/constants/string-values.constant.ts new file mode 100644 index 0000000..4d93272 --- /dev/null +++ b/src/constants/string-values.constant.ts @@ -0,0 +1,10 @@ +export const ASTERISK: string = "*"; +export const DOLLAR_SIGN: string = "$"; +export const DOT: string = "."; +export const EMPTY: string = ""; +export const HASH: string = "#"; +export const HYPHEN: string = "-"; +export const LEFT_BRACE: string = "{"; +export const RIGHT_BRACE: string = "}"; +export const SPACE: string = " "; +export const UNDERSCORE: string = "_"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..526c6b7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from "./libs"; diff --git a/src/libs/camel-case/camel-case.lib.bench.ts b/src/libs/camel-case/camel-case.lib.bench.ts new file mode 100644 index 0000000..7171e79 --- /dev/null +++ b/src/libs/camel-case/camel-case.lib.bench.ts @@ -0,0 +1,95 @@ +import { describe, test, TestContext } from "vitest"; + +import { camelCase } from "./camel-case.lib"; + +const shortPascalCase: string = "XMLHttpRequestResponseCodeHandler"; +const input1KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(20); +const input10KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(200); +const input100KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(2000); +const longUnicodeInput: string = "Привет-Мир café-noir 你好-世界 こんにちは-世界 مرحبا-بالعالم ".repeat(100); +const separatorHeavyInput: string = "---foo---bar___baz...qux!!!".repeat(1000); +const mixedCaseInput: string = "aAaAaAaAaAaAaAaAaAaAaAaAaAaAaA".repeat(1000); + +describe("camelCase", (): void => { + test("given common short ASCII inputs", ({ bench }: TestContext): void => { + bench("hyphen-separated lowercase", (): void => { + camelCase("foo-bar"); + }); + + bench("already camelCase", (): void => { + camelCase("fooBar"); + }); + + bench("PascalCase", (): void => { + camelCase(shortPascalCase); + }); + + bench("acronym followed by word", (): void => { + camelCase("HTTPResponseCode"); + }); + + bench("mixed words and numbers", (): void => { + camelCase("version-2-0"); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + camelCase(input1KB); + }); + + bench("approximately 10 KB input", (): void => { + camelCase(input10KB); + }); + + bench("approximately 100 KB input", (): void => { + camelCase(input100KB); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("long Unicode input", (): void => { + camelCase(longUnicodeInput); + }); + + bench("combining marks", (): void => { + camelCase("cafe\u0301 noir"); + }); + }); + + test("given separator-heavy input", ({ bench }: TestContext): void => { + bench("long separator-heavy input", (): void => { + camelCase(separatorHeavyInput); + }); + }); + + test("given mixed-case input", ({ bench }: TestContext): void => { + bench("long mixed-case input", (): void => { + camelCase(mixedCaseInput); + }); + }); + + test("given simple fast-path inputs", ({ bench }: TestContext): void => { + bench("single word", (): void => { + camelCase("hello"); + }); + + bench("numeric-only input", (): void => { + camelCase("123456789"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + camelCase(""); + }); + + bench("null", (): void => { + camelCase(null); + }); + + bench("undefined", (): void => { + camelCase(undefined); + }); + }); +}); diff --git a/src/libs/camel-case/camel-case.lib.spec.ts b/src/libs/camel-case/camel-case.lib.spec.ts new file mode 100644 index 0000000..453f1db --- /dev/null +++ b/src/libs/camel-case/camel-case.lib.spec.ts @@ -0,0 +1,160 @@ +import { describe, expect, test } from "vitest"; + +import { camelCase } from "./camel-case.lib"; + +describe("camelCase", (): void => { + describe("given a standard string with separators", (): void => { + test("converts separated words to camelCase", (): void => { + expect(camelCase("foo-bar")).toBe("fooBar"); + expect(camelCase("foo_bar")).toBe("fooBar"); + expect(camelCase("foo.bar")).toBe("fooBar"); + expect(camelCase("foo bar")).toBe("fooBar"); + expect(camelCase("--foo-bar--")).toBe("fooBar"); + expect(camelCase("__foo_bar__")).toBe("fooBar"); + expect(camelCase(" foo bar ")).toBe("fooBar"); + }); + + test("handles mixed separators", (): void => { + expect(camelCase("foo-bar_baz.qux")).toBe("fooBarBazQux"); + expect(camelCase("foo / bar \\ baz")).toBe("fooBarBaz"); + expect(camelCase("foo!!!bar")).toBe("fooBar"); + expect(camelCase("foo---bar")).toBe("fooBar"); + expect(camelCase("foo...bar")).toBe("fooBar"); + }); + }); + + describe("given an uppercase string with separators", (): void => { + test("normalizes uppercase words to camelCase", (): void => { + expect(camelCase("FOO-BAR")).toBe("fooBar"); + expect(camelCase("FOO_BAR")).toBe("fooBar"); + expect(camelCase("FOO BAR")).toBe("fooBar"); + expect(camelCase("FOO-BAR-BAZ")).toBe("fooBarBaz"); + }); + }); + + describe("given a PascalCase string", (): void => { + test("converts PascalCase to camelCase", (): void => { + expect(camelCase("FooBar")).toBe("fooBar"); + expect(camelCase("FooBarBaz")).toBe("fooBarBaz"); + expect(camelCase("XMLHttpRequest")).toBe("xmlHttpRequest"); + }); + }); + + describe("given a camelCase string", (): void => { + test("preserves the existing camelCase structure", (): void => { + expect(camelCase("fooBar")).toBe("fooBar"); + expect(camelCase("fooBarBaz")).toBe("fooBarBaz"); + expect(camelCase("xmlHttpRequest")).toBe("xmlHttpRequest"); + }); + }); + + describe("given acronyms and mixed casing", (): void => { + test("handles acronym boundaries correctly", (): void => { + expect(camelCase("HTTPResponseCode")).toBe("httpResponseCode"); + expect(camelCase("JSONDataAPI")).toBe("jsonDataApi"); + expect(camelCase("XMLHttpRequest")).toBe("xmlHttpRequest"); + expect(camelCase("parseHTTPResponse")).toBe("parseHttpResponse"); + expect(camelCase("getURLValue")).toBe("getUrlValue"); + }); + }); + + describe("given numbers in strings", (): void => { + test("handles numbers as separate words", (): void => { + expect(camelCase("version 2.0")).toBe("version20"); + expect(camelCase("version-2-0")).toBe("version20"); + expect(camelCase("version_2_0")).toBe("version20"); + expect(camelCase("2foo")).toBe("2Foo"); + expect(camelCase("foo2bar")).toBe("foo2Bar"); + }); + + test("preserves numeric-only input", (): void => { + expect(camelCase("123")).toBe("123"); + expect(camelCase("123-456")).toBe("123456"); + expect(camelCase("123_456_789")).toBe("123456789"); + }); + }); + + describe("given accents and Unicode letters", (): void => { + test("handles accented Latin characters", (): void => { + expect(camelCase("crème-brûlée")).toBe("crèmeBrûlée"); + expect(camelCase("jalapeño popper")).toBe("jalapeñoPopper"); + expect(camelCase("École Normale")).toBe("écoleNormale"); + }); + + test("handles Cyrillic casing", (): void => { + expect(camelCase("Привет Мир")).toBe("приветМир"); + expect(camelCase("привет-мир")).toBe("приветМир"); + expect(camelCase("ПРИВЕТ МИР")).toBe("приветМир"); + }); + + test("preserves combining marks", (): void => { + expect(camelCase("cafe\u0301 noir")).toBe("cafe\u0301Noir"); + expect(camelCase("Cafe\u0301 Noir")).toBe("cafe\u0301Noir"); + }); + }); + + describe("given non-cased scripts", (): void => { + test("joins CJK, Hiragana, and Hangul without case transformation", (): void => { + expect(camelCase("こんにちは 世界")).toBe("こんにちは世界"); + expect(camelCase("你好 世界")).toBe("你好世界"); + expect(camelCase("안녕하세요 세계")).toBe("안녕하세요세계"); + }); + + test("joins Arabic words without case transformation", (): void => { + expect(camelCase("مرحبا بالعالم")).toBe("مرحبابالعالم"); + }); + + test("handles mixed Latin and non-cased scripts", (): void => { + expect(camelCase("hello 世界")).toBe("hello世界"); + expect(camelCase("foo-こんにちは")).toBe("fooこんにちは"); + expect(camelCase("Hello 世界")).toBe("hello世界"); + }); + }); + + describe("given strings containing only symbols or separators", (): void => { + test("returns an empty string when no words are extractable", (): void => { + expect(camelCase("---")).toBe(""); + expect(camelCase("___")).toBe(""); + expect(camelCase("...")).toBe(""); + expect(camelCase("!!!")).toBe(""); + expect(camelCase("@#$%^&*")).toBe(""); + }); + }); + + describe("given a camelCase result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "foo-bar", + "FOO_BAR", + "FooBar", + "HTTPResponseCode", + "JSONDataAPI", + "version-2-0", + "crème-brûlée", + "Привет Мир", + "hello 世界" + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = camelCase(input); + + expect(camelCase(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(camelCase("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(camelCase(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(camelCase(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/camel-case/camel-case.lib.ts b/src/libs/camel-case/camel-case.lib.ts new file mode 100644 index 0000000..1bcca16 --- /dev/null +++ b/src/libs/camel-case/camel-case.lib.ts @@ -0,0 +1,52 @@ +import { EMPTY, WORD_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Converts a string to camelCase. + * + * Handles separators, existing casing styles, acronyms, numbers, Unicode characters, and combining marks. + * + * @param value - The string to convert to camelCase, or `null` or `undefined`. + * @returns The camelCase string, or an empty string when `value` is empty, `null`, or `undefined`, or contains no extractable words. + * + * @example + * + * camelCase("Foo Bar"); // "fooBar" + * camelCase("--foo-bar--"); // "fooBar" + * camelCase("FOO_BAR"); // "fooBar" + * camelCase("fooBar"); // "fooBar" + * camelCase("XMLHttpRequest"); // "xmlHttpRequest" + * camelCase("version 2.0"); // "version20" + * camelCase("Привет Мир"); // "приветМир" + * camelCase("你好 世界"); // "你好世界" + * camelCase("cafe\u0301 noir"); // "cafe\u0301Noir" + * camelCase("---"); // "" + * camelCase(""); // "" + * camelCase(null); // "" + * camelCase(undefined); // "" + */ +export function camelCase(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const words: RegExpMatchArray | null = value.match(WORD_REGEX); + + if (!words) { + return EMPTY; + } + + const length: number = words.length; + const firstWord: string = words[0]; + + let result: string = firstWord.toLowerCase(); + + for (let index: number = 1; index < length; index++) { + const word: string = words[index]; + + result += word[0].toUpperCase() + word.slice(1).toLowerCase(); + } + + return result; +} diff --git a/src/libs/camel-case/index.ts b/src/libs/camel-case/index.ts new file mode 100644 index 0000000..2f24c91 --- /dev/null +++ b/src/libs/camel-case/index.ts @@ -0,0 +1 @@ +export * from "./camel-case.lib"; diff --git a/src/libs/capitalize/capitalize.lib.bench.ts b/src/libs/capitalize/capitalize.lib.bench.ts new file mode 100644 index 0000000..f2c7f50 --- /dev/null +++ b/src/libs/capitalize/capitalize.lib.bench.ts @@ -0,0 +1,124 @@ +import { describe, test, TestContext } from "vitest"; + +import { capitalize } from "./capitalize.lib"; + +const longLowercaseString: string = "the quick brown fox jumps over the lazy dog ".repeat(100); +const longCapitalizedString: string = "The quick brown fox jumps over the lazy dog ".repeat(100); +const longCamelCaseString: string = "xmlHttpRequestResponseCodeHandler".repeat(100); +const longSymbols: string = "!@#$%^&*()_+-=[]{}|;:',.<>?/`~".repeat(100); +const longWhitespace: string = " ".repeat(1000); +const input1KB: string = "the quick brown fox jumps over the lazy dog ".repeat(25); +const input10KB: string = "the quick brown fox jumps over the lazy dog ".repeat(250); +const input100KB: string = "the quick brown fox jumps over the lazy dog ".repeat(2500); + +describe("capitalize", (): void => { + test("given common short ASCII inputs", ({ bench }: TestContext): void => { + bench("lowercase first character", (): void => { + capitalize("hello"); + }); + + bench("uppercase first character (no-op)", (): void => { + capitalize("Hello"); + }); + + bench("mixed-case string", (): void => { + capitalize("hELLO world"); + }); + + bench("single character", (): void => { + capitalize("a"); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("long lowercase string", (): void => { + capitalize(longLowercaseString); + }); + + bench("long already-capitalized string", (): void => { + capitalize(longCapitalizedString); + }); + + bench("long camelCase string", (): void => { + capitalize(longCamelCaseString); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + capitalize(input1KB); + }); + + bench("approximately 10 KB input", (): void => { + capitalize(input10KB); + }); + + bench("approximately 100 KB input", (): void => { + capitalize(input100KB); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("accented Latin", (): void => { + capitalize("élève"); + }); + + bench("combining marks", (): void => { + capitalize("cafe\u0301"); + }); + + bench("Cyrillic", (): void => { + capitalize("привет мир"); + }); + + bench("CJK", (): void => { + capitalize("你好世界"); + }); + + bench("Thai", (): void => { + capitalize("สวัสดี"); + }); + + bench("Arabic", (): void => { + capitalize("مرحبا بالعالم"); + }); + }); + + test("given non-transformable leading characters", ({ bench }: TestContext): void => { + bench("leading whitespace", (): void => { + capitalize(" hello world"); + }); + + bench("leading separator", (): void => { + capitalize("---hello world"); + }); + + bench("symbols only", (): void => { + capitalize(longSymbols); + }); + + bench("whitespace only", (): void => { + capitalize(longWhitespace); + }); + }); + + test("given a Unicode uppercase mapping", ({ bench }: TestContext): void => { + bench("character with multi-character uppercase mapping", (): void => { + capitalize("ßeta"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + capitalize(""); + }); + + bench("null", (): void => { + capitalize(null); + }); + + bench("undefined", (): void => { + capitalize(undefined); + }); + }); +}); diff --git a/src/libs/capitalize/capitalize.lib.spec.ts b/src/libs/capitalize/capitalize.lib.spec.ts new file mode 100644 index 0000000..37f99b4 --- /dev/null +++ b/src/libs/capitalize/capitalize.lib.spec.ts @@ -0,0 +1,125 @@ +import { describe, expect, expectTypeOf, test } from "vitest"; + +import { capitalize } from "./capitalize.lib"; + +describe("capitalize", (): void => { + describe("given a standard string", (): void => { + test("capitalizes the first lowercase character", (): void => { + expect(capitalize("hello")).toBe("Hello"); + expect(capitalize("hello world")).toBe("Hello world"); + expect(capitalize("typescript")).toBe("Typescript"); + }); + + test("preserves an already uppercase first character", (): void => { + expect(capitalize("Hello")).toBe("Hello"); + expect(capitalize("HELLO")).toBe("HELLO"); + expect(capitalize("Hello World")).toBe("Hello World"); + }); + + test("capitalizes only the first character", (): void => { + expect(capitalize("hELLO")).toBe("HELLO"); + expect(capitalize("hELLO wORLD")).toBe("HELLO wORLD"); + expect(capitalize("helloWORLD")).toBe("HelloWORLD"); + }); + + test("handles a single-character string", (): void => { + expect(capitalize("a")).toBe("A"); + expect(capitalize("A")).toBe("A"); + }); + }); + + describe("given leading whitespace", (): void => { + test("preserves leading whitespace without capitalizing the next character", (): void => { + expect(capitalize(" hello")).toBe(" hello"); + expect(capitalize("\thello")).toBe("\thello"); + expect(capitalize("\nhello")).toBe("\nhello"); + expect(capitalize("\r\nhello")).toBe("\r\nhello"); + expect(capitalize("\u00A0hello")).toBe("\u00A0hello"); + expect(capitalize("\u3000hello")).toBe("\u3000hello"); + }); + + test("preserves whitespace-only strings", (): void => { + expect(capitalize(" ")).toBe(" "); + expect(capitalize("\t")).toBe("\t"); + expect(capitalize("\n")).toBe("\n"); + expect(capitalize("\r\n")).toBe("\r\n"); + }); + }); + + describe("given Unicode and accented characters", (): void => { + test("capitalizes supported cased Unicode characters", (): void => { + expect(capitalize("éclair")).toBe("Éclair"); + expect(capitalize("ñandú")).toBe("Ñandú"); + expect(capitalize("über")).toBe("Über"); + expect(capitalize("ørebro")).toBe("Ørebro"); + expect(capitalize("σigma")).toBe("Σigma"); + }); + + test("preserves scripts without case transformation", (): void => { + expect(capitalize("สวัสดี")).toBe("สวัสดี"); + expect(capitalize("こんにちは")).toBe("こんにちは"); + }); + + test("preserves combining marks after the first character", (): void => { + expect(capitalize("cafe\u0301")).toBe("Cafe\u0301"); + expect(capitalize("e\u0301clair")).toBe("E\u0301clair"); + }); + + test("handles uppercase mappings that contain multiple characters", (): void => { + expect(capitalize("ßeta")).toBe("SSeta"); + }); + }); + + describe("given a string beginning with a non-letter character", (): void => { + test("preserves the string when the first character is a number", (): void => { + expect(capitalize("123hello")).toBe("123hello"); + }); + + test("preserves the string when the first character is a symbol", (): void => { + expect(capitalize("!hello")).toBe("!hello"); + expect(capitalize("@hello")).toBe("@hello"); + expect(capitalize("-hello")).toBe("-hello"); + expect(capitalize("_hello")).toBe("_hello"); + }); + + test("preserves the string when the first character is an emoji", (): void => { + expect(capitalize("🔥hello")).toBe("🔥hello"); + expect(capitalize("❤️hello")).toBe("❤️hello"); + expect(capitalize("©hello")).toBe("©hello"); + }); + }); + + describe("given a string literal", (): void => { + test("preserves the transformed literal type", (): void => { + expectTypeOf(capitalize("hello")).toEqualTypeOf<"Hello">(); + expectTypeOf(capitalize("Hello")).toEqualTypeOf<"Hello">(); + }); + }); + + describe("given an already capitalized result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = ["hello", "Hello", "HELLO", "éclair", "Éclair", "σigma", "123hello", "🔥hello"]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = capitalize(input); + + expect(capitalize(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(capitalize("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(capitalize(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(capitalize(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/capitalize/capitalize.lib.ts b/src/libs/capitalize/capitalize.lib.ts new file mode 100644 index 0000000..484bf3d --- /dev/null +++ b/src/libs/capitalize/capitalize.lib.ts @@ -0,0 +1,45 @@ +import { EMPTY } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Capitalizes the first character of a string while preserving its literal type. + * + * @param value - The string to capitalize. + * @returns The capitalized string with its literal type preserved. + * + * @example + * + * capitalize("hello"); // "Hello" + * capitalize("helloWorld"); // "HelloWorld" + * capitalize("Hello"); // "Hello" + */ +export function capitalize(value: T): Capitalize; + +/** + * Capitalizes the first character of a string. + * + * @param value - The string to capitalize, or `null` or `undefined`. + * @returns The capitalized string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * capitalize("hello"); // "Hello" + * capitalize("helloWorld"); // "HelloWorld" + * capitalize("Hello"); // "Hello" + * capitalize(""); // "" + * capitalize(null); // "" + * capitalize(undefined); // "" + */ +export function capitalize(value: string | null | undefined): string; + +export function capitalize(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const firstChar: string = value[0]; + const upperFirst: string = firstChar.toUpperCase(); + + return firstChar === upperFirst ? value : upperFirst + value.slice(1); +} diff --git a/src/libs/capitalize/index.ts b/src/libs/capitalize/index.ts new file mode 100644 index 0000000..e930c16 --- /dev/null +++ b/src/libs/capitalize/index.ts @@ -0,0 +1 @@ +export * from "./capitalize.lib"; diff --git a/src/libs/collapse-whitespace/collapse-whitespace.lib.bench.ts b/src/libs/collapse-whitespace/collapse-whitespace.lib.bench.ts new file mode 100644 index 0000000..2363328 --- /dev/null +++ b/src/libs/collapse-whitespace/collapse-whitespace.lib.bench.ts @@ -0,0 +1,101 @@ +import { describe, test, TestContext } from "vitest"; + +import { collapseWhitespace } from "./collapse-whitespace.lib"; + +const normalizedShort: string = "Hello World"; +const mixedWhitespaceShort: string = " \tHello \n\t World \r "; +const normalizedLong: string = "the quick brown fox jumps over the lazy dog ".repeat(100); +const mixedWhitespaceLong: string = "the quick\tbrown\n\nfox jumps\r\nover the\tlazy dog ".repeat(100); +const input1KB: string = "the quick\tbrown\nfox jumps\r\nover the\tlazy dog ".repeat(20); +const input10KB: string = "the quick\tbrown\nfox jumps\r\nover the\tlazy dog ".repeat(200); +const input100KB: string = "the quick\tbrown\nfox jumps\r\nover the\tlazy dog ".repeat(2000); +const separatorHeavyInput: string = "foo bar".repeat(1000); +const whitespaceOnlyInput: string = " ".repeat(1000); + +const mixedUnicodeWhitespace: string = "Hello\u00A0\u3000\u2009World\u2003\u00A0".repeat(100); + +describe("collapseWhitespace", (): void => { + test("given common short inputs", ({ bench }: TestContext): void => { + bench("already normalized", (): void => { + collapseWhitespace(normalizedShort); + }); + + bench("consecutive spaces", (): void => { + collapseWhitespace("Hello World"); + }); + + bench("leading and trailing whitespace", (): void => { + collapseWhitespace(" Hello World "); + }); + + bench("mixed whitespace", (): void => { + collapseWhitespace(mixedWhitespaceShort); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("already normalized long input", (): void => { + collapseWhitespace(normalizedLong); + }); + + bench("mixed whitespace long input", (): void => { + collapseWhitespace(mixedWhitespaceLong); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + collapseWhitespace(input1KB); + }); + + bench("approximately 10 KB input", (): void => { + collapseWhitespace(input10KB); + }); + + bench("approximately 100 KB input", (): void => { + collapseWhitespace(input100KB); + }); + }); + + test("given Unicode whitespace", ({ bench }: TestContext): void => { + bench("non-breaking spaces", (): void => { + collapseWhitespace("Hello\u00A0\u00A0World"); + }); + + bench("ideographic spaces", (): void => { + collapseWhitespace("Hello\u3000\u3000World"); + }); + + bench("thin and em spaces", (): void => { + collapseWhitespace("Hello\u2009\u2003World"); + }); + + bench("mixed Unicode whitespace", (): void => { + collapseWhitespace(mixedUnicodeWhitespace); + }); + }); + + test("given separator-heavy input", ({ bench }: TestContext): void => { + bench("long repeated spaces", (): void => { + collapseWhitespace(separatorHeavyInput); + }); + + bench("whitespace-only input", (): void => { + collapseWhitespace(whitespaceOnlyInput); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + collapseWhitespace(""); + }); + + bench("null", (): void => { + collapseWhitespace(null); + }); + + bench("undefined", (): void => { + collapseWhitespace(undefined); + }); + }); +}); diff --git a/src/libs/collapse-whitespace/collapse-whitespace.lib.spec.ts b/src/libs/collapse-whitespace/collapse-whitespace.lib.spec.ts new file mode 100644 index 0000000..aa3b963 --- /dev/null +++ b/src/libs/collapse-whitespace/collapse-whitespace.lib.spec.ts @@ -0,0 +1,106 @@ +import { describe, expect, test } from "vitest"; + +import { collapseWhitespace } from "./collapse-whitespace.lib"; + +describe("collapseWhitespace", (): void => { + describe("given a string with normal spacing", (): void => { + test("returns the original string when no normalization is needed", (): void => { + expect(collapseWhitespace("Hello World")).toBe("Hello World"); + expect(collapseWhitespace("Hello")).toBe("Hello"); + }); + + test("preserves a single space between words", (): void => { + expect(collapseWhitespace("Hello World TypeScript")).toBe("Hello World TypeScript"); + }); + + test("collapses consecutive spaces into a single space", (): void => { + expect(collapseWhitespace("Hello World")).toBe("Hello World"); + expect(collapseWhitespace("Hello World TypeScript")).toBe("Hello World TypeScript"); + }); + + test("removes leading and trailing spaces", (): void => { + expect(collapseWhitespace(" Hello World ")).toBe("Hello World"); + }); + }); + + describe("given a string with special whitespace", (): void => { + test("collapses tabs, newlines, and carriage returns", (): void => { + expect(collapseWhitespace("Hello\tWorld")).toBe("Hello World"); + expect(collapseWhitespace("Hello\nWorld")).toBe("Hello World"); + expect(collapseWhitespace("Hello\rWorld")).toBe("Hello World"); + expect(collapseWhitespace("Hello\n\t\rWorld")).toBe("Hello World"); + }); + + test("collapses non-breaking spaces", (): void => { + expect(collapseWhitespace("Hello\u00A0World")).toBe("Hello World"); + expect(collapseWhitespace("\u00A0\u00A0Hello\u00A0World\u00A0\u00A0")).toBe("Hello World"); + }); + + test("collapses ideographic spaces", (): void => { + expect(collapseWhitespace("Hello\u3000World")).toBe("Hello World"); + }); + + test("collapses thin spaces and em spaces", (): void => { + expect(collapseWhitespace("Hello\u2009World\u2003TypeScript")).toBe("Hello World TypeScript"); + }); + }); + + describe("given a string with mixed whitespace", (): void => { + test("collapses mixed whitespace characters into single spaces", (): void => { + expect(collapseWhitespace(" \tHello \n\t World \r ")).toBe("Hello World"); + }); + + test("removes leading and trailing mixed whitespace", (): void => { + expect(collapseWhitespace("\t\n Hello World \u00A0\r")).toBe("Hello World"); + }); + }); + + describe("given a whitespace-only string", (): void => { + test("returns an empty string", (): void => { + expect(collapseWhitespace(" \n\t\u00A0\u3000 ")).toBe(""); + }); + }); + + describe("given a string containing multiple whitespace boundaries", (): void => { + test("normalizes every whitespace boundary independently", (): void => { + expect(collapseWhitespace(" Hello\t\tWorld\n\nTypeScript\u00A0\u00A0Rocks ")).toBe( + "Hello World TypeScript Rocks" + ); + }); + }); + + describe("given an already normalized result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "Hello", + "Hello World", + "Hello World TypeScript", + "Hello\tWorld", + " Hello World ", + "\u00A0Hello\u3000World\u2003", + " \n\t " + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = collapseWhitespace(input); + + expect(collapseWhitespace(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(collapseWhitespace("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(collapseWhitespace(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(collapseWhitespace(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/collapse-whitespace/collapse-whitespace.lib.ts b/src/libs/collapse-whitespace/collapse-whitespace.lib.ts new file mode 100644 index 0000000..e1a5436 --- /dev/null +++ b/src/libs/collapse-whitespace/collapse-whitespace.lib.ts @@ -0,0 +1,30 @@ +import { CONSECUTIVE_WHITESPACE_REGEX, EMPTY, SPACE, WHITESPACE_TEST_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Collapses consecutive whitespace characters into a single space and trims + * leading and trailing whitespace. + * + * @param value - The string value to normalize, or `null` or `undefined`. + * @returns The normalized string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * collapseWhitespace("Hello World"); // "Hello World" + * collapseWhitespace(" Hello World "); // "Hello World" + * collapseWhitespace("Hello\tWorld\n!"); // "Hello World !" + * collapseWhitespace("Hello\u00A0World"); // "Hello World" (NBSP) + * collapseWhitespace("Hello\u3000World"); // "Hello World" (Ideographic space) + * collapseWhitespace("Hello World"); // "Hello World" + * collapseWhitespace(""); // "" + * collapseWhitespace(null); // "" + * collapseWhitespace(undefined); // "" + */ +export function collapseWhitespace(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + return !WHITESPACE_TEST_REGEX.test(value) ? value : value.replace(CONSECUTIVE_WHITESPACE_REGEX, SPACE).trim(); +} diff --git a/src/libs/collapse-whitespace/index.ts b/src/libs/collapse-whitespace/index.ts new file mode 100644 index 0000000..b353e59 --- /dev/null +++ b/src/libs/collapse-whitespace/index.ts @@ -0,0 +1 @@ +export * from "./collapse-whitespace.lib"; diff --git a/src/libs/constant-case/constant-case.lib.bench.ts b/src/libs/constant-case/constant-case.lib.bench.ts new file mode 100644 index 0000000..2072c97 --- /dev/null +++ b/src/libs/constant-case/constant-case.lib.bench.ts @@ -0,0 +1,111 @@ +import { describe, test, TestContext } from "vitest"; + +import { constantCase } from "./constant-case.lib"; + +const shortPascalCase: string = "XMLHttpRequestResponseCodeHandler"; +const input1KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(20); +const input10KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(200); +const input100KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(2000); +const longUnicodeInput: string = "Привет-Мир café-noir 你好-世界 こんにちは-世界 مرحبا-بالعالم ".repeat(100); +const separatorHeavyInput: string = "---foo---bar___baz...qux!!!".repeat(1000); +const mixedCaseInput: string = "aAaAaAaAaAaAaAaAaAaAaAaAaAaAaA".repeat(1000); + +describe("constantCase", (): void => { + test("given common short ASCII inputs", ({ bench }: TestContext): void => { + bench("hyphen-separated lowercase", (): void => { + constantCase("foo-bar"); + }); + + bench("already CONSTANT_CASE", (): void => { + constantCase("FOO_BAR"); + }); + + bench("camelCase", (): void => { + constantCase("fooBar"); + }); + + bench("PascalCase", (): void => { + constantCase(shortPascalCase); + }); + + bench("acronym followed by word", (): void => { + constantCase("HTTPResponseCode"); + }); + + bench("mixed words and numbers", (): void => { + constantCase("version-2-0"); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + constantCase(input1KB); + }); + + bench("approximately 10 KB input", (): void => { + constantCase(input10KB); + }); + + bench("approximately 100 KB input", (): void => { + constantCase(input100KB); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("long Unicode input", (): void => { + constantCase(longUnicodeInput); + }); + + bench("combining marks", (): void => { + constantCase("cafe\u0301 noir"); + }); + + bench("Cyrillic", (): void => { + constantCase("Привет Мир"); + }); + + bench("CJK", (): void => { + constantCase("你好 世界"); + }); + + bench("Arabic", (): void => { + constantCase("مرحبا بالعالم"); + }); + }); + + test("given separator-heavy input", ({ bench }: TestContext): void => { + bench("long separator-heavy input", (): void => { + constantCase(separatorHeavyInput); + }); + }); + + test("given mixed-case input", ({ bench }: TestContext): void => { + bench("long mixed-case input", (): void => { + constantCase(mixedCaseInput); + }); + }); + + test("given simple inputs", ({ bench }: TestContext): void => { + bench("single word", (): void => { + constantCase("hello"); + }); + + bench("numeric-only input", (): void => { + constantCase("123456789"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + constantCase(""); + }); + + bench("null", (): void => { + constantCase(null); + }); + + bench("undefined", (): void => { + constantCase(undefined); + }); + }); +}); diff --git a/src/libs/constant-case/constant-case.lib.spec.ts b/src/libs/constant-case/constant-case.lib.spec.ts new file mode 100644 index 0000000..10cbade --- /dev/null +++ b/src/libs/constant-case/constant-case.lib.spec.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from "vitest"; + +import { constantCase } from "./constant-case.lib"; + +describe("constantCase", (): void => { + describe("given a standard string with separators", (): void => { + test("converts separated words to CONSTANT_CASE", (): void => { + expect(constantCase("foo-bar")).toBe("FOO_BAR"); + expect(constantCase("foo_bar")).toBe("FOO_BAR"); + expect(constantCase("foo.bar")).toBe("FOO_BAR"); + expect(constantCase("foo bar")).toBe("FOO_BAR"); + expect(constantCase("--foo-bar--")).toBe("FOO_BAR"); + expect(constantCase("__foo_bar__")).toBe("FOO_BAR"); + expect(constantCase(" foo bar ")).toBe("FOO_BAR"); + }); + + test("handles mixed separators", (): void => { + expect(constantCase("foo-bar_baz.qux")).toBe("FOO_BAR_BAZ_QUX"); + expect(constantCase("foo / bar \\ baz")).toBe("FOO_BAR_BAZ"); + expect(constantCase("foo!!!bar")).toBe("FOO_BAR"); + expect(constantCase("foo---bar")).toBe("FOO_BAR"); + expect(constantCase("foo...bar")).toBe("FOO_BAR"); + }); + }); + + describe("given an uppercase string with separators", (): void => { + test("normalizes uppercase words to CONSTANT_CASE", (): void => { + expect(constantCase("FOO-BAR")).toBe("FOO_BAR"); + expect(constantCase("FOO_BAR")).toBe("FOO_BAR"); + expect(constantCase("FOO BAR")).toBe("FOO_BAR"); + expect(constantCase("FOO-BAR-BAZ")).toBe("FOO_BAR_BAZ"); + }); + }); + + describe("given camelCase and PascalCase strings", (): void => { + test("converts camelCase to CONSTANT_CASE", (): void => { + expect(constantCase("fooBar")).toBe("FOO_BAR"); + expect(constantCase("xmlHttpRequest")).toBe("XML_HTTP_REQUEST"); + }); + + test("converts PascalCase to CONSTANT_CASE", (): void => { + expect(constantCase("FooBar")).toBe("FOO_BAR"); + expect(constantCase("XmlHttpRequest")).toBe("XML_HTTP_REQUEST"); + }); + }); + + describe("given acronyms and mixed casing", (): void => { + test("splits acronyms at word boundaries", (): void => { + expect(constantCase("HTTPResponseCode")).toBe("HTTP_RESPONSE_CODE"); + expect(constantCase("JSONDataAPI")).toBe("JSON_DATA_API"); + expect(constantCase("XMLHttpRequest")).toBe("XML_HTTP_REQUEST"); + expect(constantCase("parseHTTPResponse")).toBe("PARSE_HTTP_RESPONSE"); + expect(constantCase("getURLValue")).toBe("GET_URL_VALUE"); + }); + }); + + describe("given strings containing numbers", (): void => { + test("handles numbers as separate words", (): void => { + expect(constantCase("version 2.0")).toBe("VERSION_2_0"); + expect(constantCase("version-2-0")).toBe("VERSION_2_0"); + expect(constantCase("version_2_0")).toBe("VERSION_2_0"); + expect(constantCase("2foo")).toBe("2_FOO"); + expect(constantCase("foo2bar")).toBe("FOO_2_BAR"); + }); + + test("preserves numeric-only words", (): void => { + expect(constantCase("123")).toBe("123"); + expect(constantCase("123-456")).toBe("123_456"); + expect(constantCase("123_456_789")).toBe("123_456_789"); + }); + }); + + describe("given accents and Unicode letters", (): void => { + test("preserves accented letters while uppercasing words", (): void => { + expect(constantCase("crème-brûlée")).toBe("CRÈME_BRÛLÉE"); + expect(constantCase("jalapeño popper")).toBe("JALAPEÑO_POPPER"); + expect(constantCase("École Normale")).toBe("ÉCOLE_NORMALE"); + }); + + test("converts Cyrillic words to uppercase", (): void => { + expect(constantCase("Привет Мир")).toBe("ПРИВЕТ_МИР"); + expect(constantCase("привет-мир")).toBe("ПРИВЕТ_МИР"); + expect(constantCase("ПРИВЕТ МИР")).toBe("ПРИВЕТ_МИР"); + }); + }); + + describe("given non-cased scripts", (): void => { + test("joins CJK, Hiragana, and Hangul words with underscores", (): void => { + expect(constantCase("こんにちは 世界")).toBe("こんにちは_世界"); + expect(constantCase("你好 世界")).toBe("你好_世界"); + expect(constantCase("안녕하세요 세계")).toBe("안녕하세요_세계"); + }); + + test("joins Arabic words with underscores without case transformation", (): void => { + expect(constantCase("مرحبا بالعالم")).toBe("مرحبا_بالعالم"); + }); + + test("handles mixed Latin and non-cased scripts", (): void => { + expect(constantCase("hello 世界")).toBe("HELLO_世界"); + expect(constantCase("foo-こんにちは")).toBe("FOO_こんにちは"); + expect(constantCase("hello 世界 Привет")).toBe("HELLO_世界_ПРИВЕТ"); + }); + }); + + describe("given decomposed Unicode characters", (): void => { + test("preserves combining marks", (): void => { + expect(constantCase("cafe\u0301 noir")).toBe("CAFE\u0301_NOIR"); + expect(constantCase("Cafe\u0301 Noir")).toBe("CAFE\u0301_NOIR"); + }); + }); + + describe("given strings with no extractable words", (): void => { + test("returns an empty string for symbol-only inputs", (): void => { + expect(constantCase("---")).toBe(""); + expect(constantCase("___")).toBe(""); + expect(constantCase("...")).toBe(""); + expect(constantCase("!!!")).toBe(""); + expect(constantCase("@#$%^&*")).toBe(""); + }); + }); + + describe("given a CONSTANT_CASE result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "foo-bar", + "FOO_BAR", + "fooBar", + "FooBar", + "HTTPResponseCode", + "JSONDataAPI", + "version-2-0", + "crème-brûlée", + "Привет Мир", + "hello 世界" + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = constantCase(input); + + expect(constantCase(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(constantCase("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(constantCase(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(constantCase(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/constant-case/constant-case.lib.ts b/src/libs/constant-case/constant-case.lib.ts new file mode 100644 index 0000000..81c5285 --- /dev/null +++ b/src/libs/constant-case/constant-case.lib.ts @@ -0,0 +1,48 @@ +import { EMPTY, UNDERSCORE, WORD_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Converts a string to CONSTANT_CASE (SCREAMING_SNAKE_CASE). + * + * Handles separators, existing casing styles, acronyms, numbers, Unicode characters, and combining marks. + * + * @param value - The string to convert to CONSTANT_CASE, or `null` or `undefined`. + * @returns The CONSTANT_CASE string, or an empty string when `value` is empty, `null`, or `undefined`, or contains no extractable words. + * + * @example + * + * constantCase("Foo Bar"); // "FOO_BAR" + * constantCase("--foo-bar--"); // "FOO_BAR" + * constantCase("fooBar"); // "FOO_BAR" + * constantCase("FOO_BAR"); // "FOO_BAR" + * constantCase("XMLHttpRequest"); // "XML_HTTP_REQUEST" + * constantCase("version 2.0"); // "VERSION_2_0" + * constantCase("Привет Мир"); // "ПРИВЕТ_МИР" + * constantCase("你好 世界"); // "你好_世界" + * constantCase("cafe\u0301 noir"); // "CAFE\u0301_NOIR" + * constantCase(""); // "" + * constantCase(null); // "" + * constantCase(undefined); // "" + */ +export function constantCase(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const words: RegExpMatchArray | null = value.match(WORD_REGEX); + + if (!words) { + return EMPTY; + } + + const length: number = words.length; + + let result: string = words[0].toUpperCase(); + + for (let index: number = 1; index < length; index++) { + result += UNDERSCORE + words[index].toUpperCase(); + } + + return result; +} diff --git a/src/libs/constant-case/index.ts b/src/libs/constant-case/index.ts new file mode 100644 index 0000000..783132e --- /dev/null +++ b/src/libs/constant-case/index.ts @@ -0,0 +1 @@ +export * from "./constant-case.lib"; diff --git a/src/libs/default-if-blank/default-if-blank.lib.bench.ts b/src/libs/default-if-blank/default-if-blank.lib.bench.ts new file mode 100644 index 0000000..f561738 --- /dev/null +++ b/src/libs/default-if-blank/default-if-blank.lib.bench.ts @@ -0,0 +1,102 @@ +import { describe, test, TestContext } from "vitest"; + +import { defaultIfBlank } from "./default-if-blank.lib"; + +const longText: string = "the quick brown fox jumps over the lazy dog ".repeat(100); +const longWhitespace: string = " ".repeat(1000); +const longWhitespaceWithTextAtStart: string = "a" + " ".repeat(1000); +const longWhitespaceWithTextAtEnd: string = " ".repeat(1000) + "a"; +const longMixedWhitespace: string = " \t\n\r\u00A0\u2003\u202F\u3000 ".repeat(100); +const longTextWithSurroundingWhitespace: string = " \t\n" + longText + "\u00A0\u3000 "; + +describe("defaultIfBlank", (): void => { + test("given common short inputs", ({ bench }: TestContext): void => { + bench("non-blank text", (): void => { + defaultIfBlank("hello", "fallback"); + }); + + bench("single non-whitespace character", (): void => { + defaultIfBlank("a", "fallback"); + }); + + bench("text with surrounding whitespace", (): void => { + defaultIfBlank(" hello ", "fallback"); + }); + + bench("whitespace-only", (): void => { + defaultIfBlank(" ", "fallback"); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("long non-blank text", (): void => { + defaultIfBlank(longText, "fallback"); + }); + + bench("long whitespace-only input", (): void => { + defaultIfBlank(longWhitespace, "fallback"); + }); + + bench("non-whitespace at the beginning", (): void => { + defaultIfBlank(longWhitespaceWithTextAtStart, "fallback"); + }); + + bench("non-whitespace at the end", (): void => { + defaultIfBlank(longWhitespaceWithTextAtEnd, "fallback"); + }); + + bench("long text with surrounding whitespace", (): void => { + defaultIfBlank(longTextWithSurroundingWhitespace, "fallback"); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("CJK", (): void => { + defaultIfBlank("你好世界", "fallback"); + }); + + bench("Arabic", (): void => { + defaultIfBlank("مرحبا بالعالم", "fallback"); + }); + + bench("Thai", (): void => { + defaultIfBlank("สวัสดีชาวโลก", "fallback"); + }); + + bench("accented Latin", (): void => { + defaultIfBlank("café", "fallback"); + }); + + bench("combining marks", (): void => { + defaultIfBlank("cafe\u0301", "fallback"); + }); + }); + + test("given Unicode whitespace", ({ bench }: TestContext): void => { + bench("non-breaking spaces", (): void => { + defaultIfBlank("\u00A0\u00A0\u00A0", "fallback"); + }); + + bench("ideographic spaces", (): void => { + defaultIfBlank("\u3000\u3000\u3000", "fallback"); + }); + + bench("mixed Unicode whitespace", (): void => { + defaultIfBlank(longMixedWhitespace, "fallback"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + defaultIfBlank("", "fallback"); + }); + + bench("null", (): void => { + defaultIfBlank(null, "fallback"); + }); + + bench("undefined", (): void => { + defaultIfBlank(undefined, "fallback"); + }); + }); +}); diff --git a/src/libs/default-if-blank/default-if-blank.lib.spec.ts b/src/libs/default-if-blank/default-if-blank.lib.spec.ts new file mode 100644 index 0000000..8fc34b3 --- /dev/null +++ b/src/libs/default-if-blank/default-if-blank.lib.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "vitest"; + +import { defaultIfBlank } from "./default-if-blank.lib"; + +describe("defaultIfBlank", (): void => { + describe("given a non-blank string", (): void => { + test("returns the original string", (): void => { + expect(defaultIfBlank("Hello", "default")).toBe("Hello"); + expect(defaultIfBlank("Hello World", "default")).toBe("Hello World"); + expect(defaultIfBlank("12345", "default")).toBe("12345"); + }); + + test("preserves leading and trailing whitespace", (): void => { + expect(defaultIfBlank(" Hello World ", "default")).toBe(" Hello World "); + expect(defaultIfBlank("\tHello\t", "default")).toBe("\tHello\t"); + expect(defaultIfBlank("\t\n text \r\n", "default")).toBe("\t\n text \r\n"); + }); + + test("preserves Unicode and accented characters", (): void => { + expect(defaultIfBlank("สวัสดีชาวโลก", "default")).toBe("สวัสดีชาวโลก"); + expect(defaultIfBlank("こんにちは", "default")).toBe("こんにちは"); + expect(defaultIfBlank("café", "default")).toBe("café"); + expect(defaultIfBlank("مرحبا بالعالم", "default")).toBe("مرحبا بالعالم"); + expect(defaultIfBlank("cafe\u0301", "default")).toBe("cafe\u0301"); + }); + + test("preserves symbols and emojis", (): void => { + expect(defaultIfBlank("🔥", "default")).toBe("🔥"); + expect(defaultIfBlank("!@#$%^&*()", "default")).toBe("!@#$%^&*()"); + }); + + test("returns the original value when text contains surrounding whitespace", (): void => { + const value: string = " \t\n Hello World \u00A0 "; + + expect(defaultIfBlank(value, "default")).toBe(value); + }); + }); + + describe("given a whitespace-only string", (): void => { + test("returns the default value for ASCII whitespace", (): void => { + expect(defaultIfBlank(" ", "default")).toBe("default"); + expect(defaultIfBlank(" ", "default")).toBe("default"); + expect(defaultIfBlank("\t\n\r\v\f", "default")).toBe("default"); + expect(defaultIfBlank(" \t\n\r ", "default")).toBe("default"); + }); + + test("returns the default value for Unicode whitespace", (): void => { + expect(defaultIfBlank("\u00A0", "default")).toBe("default"); + expect(defaultIfBlank("\u2003", "default")).toBe("default"); + expect(defaultIfBlank("\u202F", "default")).toBe("default"); + expect(defaultIfBlank("\u3000", "default")).toBe("default"); + expect(defaultIfBlank(" \t\n\r\u00A0\u3000 ", "default")).toBe("default"); + }); + + test("returns the default value for mixed whitespace", (): void => { + expect(defaultIfBlank(" \t\n\r\u00A0\u2003\u202F\u3000 ", "default")).toBe("default"); + }); + }); + + describe("given a string containing a single non-whitespace character", (): void => { + test("returns the original string", (): void => { + expect(defaultIfBlank("a", "default")).toBe("a"); + expect(defaultIfBlank("1", "default")).toBe("1"); + expect(defaultIfBlank("🔥", "default")).toBe("🔥"); + expect(defaultIfBlank("中", "default")).toBe("中"); + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns the default value for an empty string", (): void => { + expect(defaultIfBlank("", "default")).toBe("default"); + }); + + test("returns the default value for null", (): void => { + expect(defaultIfBlank(null, "default")).toBe("default"); + }); + + test("returns the default value for undefined", (): void => { + expect(defaultIfBlank(undefined, "default")).toBe("default"); + }); + }); +}); diff --git a/src/libs/default-if-blank/default-if-blank.lib.ts b/src/libs/default-if-blank/default-if-blank.lib.ts new file mode 100644 index 0000000..4558d14 --- /dev/null +++ b/src/libs/default-if-blank/default-if-blank.lib.ts @@ -0,0 +1,20 @@ +import { hasText } from "../has-text"; + +/** + * Returns a default value when the given value is empty or contains only whitespace. + * + * @param value - The string value to check, or `null` or `undefined`. + * @param defaultValue - The value to return when `value` is empty, whitespace-only, `null`, or `undefined`. + * @returns The original `value` when it contains at least one non-whitespace character; otherwise, `defaultValue`. + * + * @example + * + * defaultIfBlank("Hello", "default"); // "Hello" + * defaultIfBlank(" ", "default"); // "default" + * defaultIfBlank("", "default"); // "default" + * defaultIfBlank(null, "default"); // "default" + * defaultIfBlank(undefined, "default"); // "default" + */ +export function defaultIfBlank(value: string | null | undefined, defaultValue: string): string { + return hasText(value) ? value : defaultValue; +} diff --git a/src/libs/default-if-blank/index.ts b/src/libs/default-if-blank/index.ts new file mode 100644 index 0000000..c87bf74 --- /dev/null +++ b/src/libs/default-if-blank/index.ts @@ -0,0 +1 @@ +export * from "./default-if-blank.lib"; diff --git a/src/libs/default-if-empty/default-if-empty.lib.bench.ts b/src/libs/default-if-empty/default-if-empty.lib.bench.ts new file mode 100644 index 0000000..a5895c9 --- /dev/null +++ b/src/libs/default-if-empty/default-if-empty.lib.bench.ts @@ -0,0 +1,125 @@ +import { describe, test, TestContext } from "vitest"; + +import { defaultIfEmpty } from "./default-if-empty.lib"; + +const longText: string = "the quick brown fox jumps over the lazy dog ".repeat(100); +const longWhitespace: string = " ".repeat(1000); +const longSymbols: string = "!@#$%^&*()_+-=[]{}|;:',.<>?/`~".repeat(50); +const longUnicode: string = "你好世界 مرحبا بالعالم สวัสดี こんにちは café ".repeat(100); +const longTextWithLeadingWhitespace: string = " ".repeat(1000) + "hello"; +const longTextWithTrailingWhitespace: string = "hello" + " ".repeat(1000); +const input1KB: string = "the quick brown fox jumps over the lazy dog ".repeat(25); +const input10KB: string = "the quick brown fox jumps over the lazy dog ".repeat(250); +const input100KB: string = "the quick brown fox jumps over the lazy dog ".repeat(2500); + +describe("defaultIfEmpty", (): void => { + test("given common short inputs", ({ bench }: TestContext): void => { + bench("non-empty text", (): void => { + defaultIfEmpty("hello", "fallback"); + }); + + bench("single character", (): void => { + defaultIfEmpty("a", "fallback"); + }); + + bench("whitespace-only", (): void => { + defaultIfEmpty(" ", "fallback"); + }); + + bench("symbol-only", (): void => { + defaultIfEmpty("!@#$%^&*", "fallback"); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("long text", (): void => { + defaultIfEmpty(longText, "fallback"); + }); + + bench("long whitespace-only input", (): void => { + defaultIfEmpty(longWhitespace, "fallback"); + }); + + bench("long symbols", (): void => { + defaultIfEmpty(longSymbols, "fallback"); + }); + + bench("long Unicode input", (): void => { + defaultIfEmpty(longUnicode, "fallback"); + }); + }); + + test("given text with surrounding whitespace", ({ bench }: TestContext): void => { + bench("long leading whitespace", (): void => { + defaultIfEmpty(longTextWithLeadingWhitespace, "fallback"); + }); + + bench("long trailing whitespace", (): void => { + defaultIfEmpty(longTextWithTrailingWhitespace, "fallback"); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + defaultIfEmpty(input1KB, "fallback"); + }); + + bench("approximately 10 KB input", (): void => { + defaultIfEmpty(input10KB, "fallback"); + }); + + bench("approximately 100 KB input", (): void => { + defaultIfEmpty(input100KB, "fallback"); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("CJK", (): void => { + defaultIfEmpty("你好世界", "fallback"); + }); + + bench("Arabic", (): void => { + defaultIfEmpty("مرحبا بالعالم", "fallback"); + }); + + bench("Thai", (): void => { + defaultIfEmpty("สวัสดีชาวโลก", "fallback"); + }); + + bench("accented Latin", (): void => { + defaultIfEmpty("café", "fallback"); + }); + + bench("combining marks", (): void => { + defaultIfEmpty("cafe\u0301", "fallback"); + }); + }); + + test("given Unicode whitespace", ({ bench }: TestContext): void => { + bench("non-breaking space", (): void => { + defaultIfEmpty("\u00A0", "fallback"); + }); + + bench("ideographic space", (): void => { + defaultIfEmpty("\u3000", "fallback"); + }); + + bench("mixed Unicode whitespace", (): void => { + defaultIfEmpty(" \t\n\r\u00A0\u2003\u202F\u3000 ", "fallback"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + defaultIfEmpty("", "fallback"); + }); + + bench("null", (): void => { + defaultIfEmpty(null, "fallback"); + }); + + bench("undefined", (): void => { + defaultIfEmpty(undefined, "fallback"); + }); + }); +}); diff --git a/src/libs/default-if-empty/default-if-empty.lib.spec.ts b/src/libs/default-if-empty/default-if-empty.lib.spec.ts new file mode 100644 index 0000000..98c5338 --- /dev/null +++ b/src/libs/default-if-empty/default-if-empty.lib.spec.ts @@ -0,0 +1,66 @@ +import { describe, expect, test } from "vitest"; + +import { defaultIfEmpty } from "./default-if-empty.lib"; + +describe("defaultIfEmpty", (): void => { + describe("given a non-empty string", (): void => { + test("returns the original string", (): void => { + expect(defaultIfEmpty("Hello", "default")).toBe("Hello"); + expect(defaultIfEmpty("Hello World", "default")).toBe("Hello World"); + expect(defaultIfEmpty("12345", "default")).toBe("12345"); + }); + + test("preserves whitespace-only strings", (): void => { + expect(defaultIfEmpty(" ", "default")).toBe(" "); + expect(defaultIfEmpty(" ", "default")).toBe(" "); + expect(defaultIfEmpty("\t\n\r", "default")).toBe("\t\n\r"); + expect(defaultIfEmpty(" \t\n\r ", "default")).toBe(" \t\n\r "); + }); + + test("preserves Unicode whitespace characters", (): void => { + expect(defaultIfEmpty("\u00A0", "default")).toBe("\u00A0"); + expect(defaultIfEmpty("\u2003", "default")).toBe("\u2003"); + expect(defaultIfEmpty("\u202F", "default")).toBe("\u202F"); + expect(defaultIfEmpty("\u3000", "default")).toBe("\u3000"); + }); + + test("preserves Unicode and accented characters", (): void => { + expect(defaultIfEmpty("สวัสดีชาวโลก", "default")).toBe("สวัสดีชาวโลก"); + expect(defaultIfEmpty("こんにちは", "default")).toBe("こんにちは"); + expect(defaultIfEmpty("café", "default")).toBe("café"); + expect(defaultIfEmpty("مرحبا بالعالم", "default")).toBe("مرحبا بالعالم"); + expect(defaultIfEmpty("cafe\u0301", "default")).toBe("cafe\u0301"); + }); + + test("preserves symbols and emojis", (): void => { + expect(defaultIfEmpty("🔥", "default")).toBe("🔥"); + expect(defaultIfEmpty("!@#$%^&*()", "default")).toBe("!@#$%^&*()"); + }); + + test("preserves strings containing only a single code unit", (): void => { + expect(defaultIfEmpty("a", "default")).toBe("a"); + expect(defaultIfEmpty("1", "default")).toBe("1"); + expect(defaultIfEmpty(" ", "default")).toBe(" "); + }); + + test("returns the exact original value without normalization", (): void => { + const value: string = " \t Hello World \n "; + + expect(defaultIfEmpty(value, "default")).toBe(value); + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns the default value for an empty string", (): void => { + expect(defaultIfEmpty("", "default")).toBe("default"); + }); + + test("returns the default value for null", (): void => { + expect(defaultIfEmpty(null, "default")).toBe("default"); + }); + + test("returns the default value for undefined", (): void => { + expect(defaultIfEmpty(undefined, "default")).toBe("default"); + }); + }); +}); diff --git a/src/libs/default-if-empty/default-if-empty.lib.ts b/src/libs/default-if-empty/default-if-empty.lib.ts new file mode 100644 index 0000000..24c1be3 --- /dev/null +++ b/src/libs/default-if-empty/default-if-empty.lib.ts @@ -0,0 +1,20 @@ +import { hasLength } from "../has-length"; + +/** + * Returns a default value when the given value is empty, `null`, or `undefined`. + * + * @param value - The string value to check, or `null` or `undefined`. + * @param defaultValue - The value to return when `value` is empty, `null`, or `undefined`. + * @returns The original `value` when it is a non-empty string; otherwise, `defaultValue`. + * + * @example + * + * defaultIfEmpty("Hello", "default"); // "Hello" + * defaultIfEmpty(" ", "default"); // " " + * defaultIfEmpty("", "default"); // "default" + * defaultIfEmpty(null, "default"); // "default" + * defaultIfEmpty(undefined, "default"); // "default" + */ +export function defaultIfEmpty(value: string | null | undefined, defaultValue: string): string { + return hasLength(value) ? value : defaultValue; +} diff --git a/src/libs/default-if-empty/index.ts b/src/libs/default-if-empty/index.ts new file mode 100644 index 0000000..e63c526 --- /dev/null +++ b/src/libs/default-if-empty/index.ts @@ -0,0 +1 @@ +export * from "./default-if-empty.lib"; diff --git a/src/libs/format/format.lib.bench.ts b/src/libs/format/format.lib.bench.ts new file mode 100644 index 0000000..41a22cf --- /dev/null +++ b/src/libs/format/format.lib.bench.ts @@ -0,0 +1,178 @@ +import { describe, test, TestContext } from "vitest"; + +import { createFormatter, format } from "./format.lib"; + +const manyTemplate: string = "{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} " + "{0} {1} {2} {3} {4} {5} {6} {7} {8} {9}"; + +const fiftyTemplate: string = Array.from( + { length: 50 }, + (_value: unknown, index: number): string => `{${index % 10}}` +).join(" "); + +const greet: (...args: unknown[]) => string = createFormatter("Hello {0}, welcome to {1}!"); +const userFmt: (...args: unknown[]) => string = createFormatter("{0.name} ({0.role}) has {0.stats.score} points"); +const fiftyFmt: (...args: unknown[]) => string = createFormatter(fiftyTemplate); +const nullFmt: (...args: unknown[]) => string = createFormatter(null); +const staticFmt: (...args: unknown[]) => string = createFormatter("static text with no braces"); + +describe("format", (): void => { + test("short ASCII", ({ bench }: TestContext): void => { + bench("single placeholder with string", (): void => { + format("Hello {0}!", "World"); + }); + + bench("two placeholders with primitives", (): void => { + format("{0} has {1} items", "Cart", 5); + }); + + bench("boolean argument", (): void => { + format("Active: {0}", true); + }); + + bench("no brace in template (early return)", (): void => { + format("Hello World, no placeholders here"); + }); + }); + + test("long input", ({ bench }: TestContext): void => { + bench("multiple indexed arguments", (): void => { + format("User {0} scored {1} points on {2}, active: {3}, role: {4}", "Alice", 9500, "2025-01-15", true, "admin"); + }); + + bench("dot-notation object access", (): void => { + format("{0.name} ({0.role}) has {0.stats.score} points", { + name: "Alice", + role: "admin", + stats: { score: 9500 } + }); + }); + + bench("dot-notation array access", (): void => { + format("First: {0.0}, Second: {0.1}, Third: {0.2}", ["Alice", "Bob", "Charlie"]); + }); + + bench("deep nested array-of-objects", (): void => { + format("{0.users.0.name} and {0.users.1.name}", { + users: [{ name: "Alice" }, { name: "Bob" }] + }); + }); + + bench("JSON.stringify fallback for whole object", (): void => { + format("Data: {0}", { key: "value", nested: { a: 1 } }); + }); + }); + + test("Unicode and combining marks", ({ bench }: TestContext): void => { + bench("CJK arguments", (): void => { + format("{0}は{1}です", "今日", "良い天気"); + }); + + bench("Thai arguments", (): void => { + format("สวัสดี {0}", "โลก"); + }); + + bench("Arabic arguments", (): void => { + format("{0} {1}", "مرحبا", "بالعالم"); + }); + + bench("combining marks in template", (): void => { + format("cafe\u0301: {0}, re\u0301sume\u0301: {1}", "open", "ready"); + }); + }); + + test("excessive placeholders", ({ bench }: TestContext): void => { + bench("20 placeholders with 10 arguments", (): void => { + format(manyTemplate, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j"); + }); + + bench("repeated same index 10 times", (): void => { + format("{0} {0} {0} {0} {0} {0} {0} {0} {0} {0}", "echo"); + }); + + bench("50 placeholders with 10 arguments", (): void => { + format(fiftyTemplate, "a", "b", "c", "d", "e", "f", "g", "h", "i", "j"); + }); + }); + + test("empty and nullish", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + format(""); + }); + + bench("null", (): void => { + format(null); + }); + + bench("undefined", (): void => { + format(undefined); + }); + }); +}); + +describe("createFormatter", (): void => { + test("compile overhead", ({ bench }: TestContext): void => { + bench("simple template", (): void => { + createFormatter("Hello {0}!"); + }); + + bench("complex dot-notation", (): void => { + createFormatter("{0.name} ({0.role}) scored {1} on {2.date}"); + }); + + bench("20 placeholders", (): void => { + createFormatter("{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} {0} {1} {2} {3} {4} {5} {6} {7} {8} {9}"); + }); + }); + + test("pre-compiled reuse", ({ bench }: TestContext): void => { + bench("simple template call", (): void => { + greet("Alice", "Wonderland"); + }); + + bench("dot-notation call", (): void => { + userFmt({ name: "Alice", role: "admin", stats: { score: 9500 } }); + }); + + bench("50-placeholder call", (): void => { + fiftyFmt("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"); + }); + + bench("no-placeholder call", (): void => { + staticFmt("ignored"); + }); + + bench("null template call", (): void => { + nullFmt("anything"); + }); + }); +}); + +describe("format vs createFormatter comparison", (): void => { + test("simple template", async ({ bench }: TestContext): Promise => { + const template: string = "Hello {0}, welcome to {1}!"; + const compiled: (...args: unknown[]) => string = createFormatter(template); + + await bench.compare( + bench("format() one-shot", (): void => { + format(template, "Alice", "Wonderland"); + }), + bench("createFormatter() pre-compiled", (): void => { + compiled("Alice", "Wonderland"); + }) + ); + }); + + test("dot-notation template", async ({ bench }: TestContext): Promise => { + const template: string = "{0.name} ({0.role}) has {0.stats.score} points"; + const compiled: (...args: unknown[]) => string = createFormatter(template); + + await bench.compare( + bench("format() dot-notation one-shot", (): void => { + format(template, { name: "Alice", role: "admin", stats: { score: 9500 } }); + }), + bench("createFormatter() dot-notation pre-compiled", (): void => { + compiled({ name: "Alice", role: "admin", stats: { score: 9500 } }); + }) + ); + }); +}); diff --git a/src/libs/format/format.lib.spec.ts b/src/libs/format/format.lib.spec.ts new file mode 100644 index 0000000..6f854a6 --- /dev/null +++ b/src/libs/format/format.lib.spec.ts @@ -0,0 +1,312 @@ +import { describe, expect, test } from "vitest"; + +import { createFormatter, format } from "./format.lib"; + +describe("format", (): void => { + describe("basic indexed placeholders", (): void => { + test("replaces a single placeholder with a string argument", (): void => { + expect(format("Hello {0}!", "World")).toBe("Hello World!"); + }); + + test("replaces multiple placeholders with their respective arguments", (): void => { + expect(format("{0} + {1} = {2}", 1, 2, 3)).toBe("1 + 2 = 3"); + }); + + test("replaces repeated occurrences of the same index", (): void => { + expect(format("{0} and {0}", "echo")).toBe("echo and echo"); + }); + + test("replaces placeholders at the start and end of the template", (): void => { + expect(format("{0}{1}", "A", "B")).toBe("AB"); + }); + + test("handles a template with only a placeholder", (): void => { + expect(format("{0}", "value")).toBe("value"); + }); + }); + + describe("primitive type arguments", (): void => { + test("converts a number argument to its string representation", (): void => { + expect(format("Count: {0}", 42)).toBe("Count: 42"); + expect(format("Pi: {0}", 3.14)).toBe("Pi: 3.14"); + expect(format("Negative: {0}", -1)).toBe("Negative: -1"); + expect(format("Zero: {0}", 0)).toBe("Zero: 0"); + }); + + test("converts NaN to the string 'NaN'", (): void => { + expect(format("Value: {0}", NaN)).toBe("Value: NaN"); + }); + + test("converts Infinity to the string 'Infinity'", (): void => { + expect(format("Value: {0}", Infinity)).toBe("Value: Infinity"); + }); + + test("converts -Infinity to the string '-Infinity'", (): void => { + expect(format("Value: {0}", -Infinity)).toBe("Value: -Infinity"); + }); + + test("converts a boolean argument to its string representation", (): void => { + expect(format("Active: {0}", true)).toBe("Active: true"); + expect(format("Disabled: {0}", false)).toBe("Disabled: false"); + }); + + test("converts a null argument to the string 'null'", (): void => { + expect(format("Value: {0}", null)).toBe("Value: null"); + }); + + test("converts an undefined argument to the string 'undefined'", (): void => { + expect(format("Value: {0}", undefined)).toBe("Value: undefined"); + }); + }); + + describe("dot-notation object access", (): void => { + test("resolves a top-level property", (): void => { + expect(format("Hello {0.name}", { name: "Alice" })).toBe("Hello Alice"); + }); + + test("resolves multiple properties from the same object", (): void => { + expect(format("{0.first} {0.last}", { first: "John", last: "Doe" })).toBe("John Doe"); + }); + + test("resolves a deeply nested property", (): void => { + const data: Record = { + users: [{ name: "Bob", age: 30 }] + }; + expect(format("{0.users.0.name} is {0.users.0.age}", data)).toBe("Bob is 30"); + }); + + test("resolves a numeric property on a nested object", (): void => { + expect( + format("Hello {0.name}, you have {0.messages.0} new messages.", { + name: "John", + messages: [5] + }) + ).toBe("Hello John, you have 5 new messages."); + }); + + test("returns 'undefined' for a missing property path", (): void => { + expect(format("{0.x.y}", { x: {} })).toBe("undefined"); + }); + + test("returns 'undefined' for a path through a null property", (): void => { + expect(format("{0.a.b}", { a: null })).toBe("undefined"); + }); + }); + + describe("dot-notation array access", (): void => { + test("resolves array elements by index", (): void => { + expect(format("First: {0.0}, Second: {0.1}", ["Alice", "Bob"])).toBe("First: Alice, Second: Bob"); + }); + + test("resolves nested arrays", (): void => { + expect(format("{0.0.0}", [["deep"]])).toBe("deep"); + }); + + test("returns 'undefined' for an out-of-bounds array index", (): void => { + expect(format("{0.5}", ["only"])).toBe("undefined"); + }); + }); + + describe("dot-notation array of objects", (): void => { + test("resolves object properties inside arrays", (): void => { + const users: Array> = [ + { name: "Alice", role: "admin" }, + { name: "Bob", role: "user" } + ]; + expect(format("{0.0.name} ({0.0.role}), {0.1.name} ({0.1.role})", users)).toBe("Alice (admin), Bob (user)"); + }); + }); + + describe("whole object and array arguments", (): void => { + test("serializes a plain object via JSON.stringify", (): void => { + expect(format("{0}", { key: "val" })).toBe('{"key":"val"}'); + }); + + test("serializes an array via JSON.stringify", (): void => { + expect(format("{0}", [1, 2, 3])).toBe("[1,2,3]"); + }); + + test("serializes a nested object via JSON.stringify", (): void => { + expect(format("{0}", { a: { b: 1 } })).toBe('{"a":{"b":1}}'); + }); + + test("returns 'undefined' for a Symbol argument", (): void => { + expect(format("{0}", Symbol("test"))).toBe("undefined"); + }); + + test("returns 'undefined' for a Function argument", (): void => { + expect(format("{0}", (): void => {})).toBe("undefined"); + }); + + test("returns 'undefined' for a circular object", (): void => { + const circular: Record = {}; + circular["self"] = circular; + expect(format("{0}", circular)).toBe("undefined"); + }); + }); + + describe("mixed type arguments", (): void => { + test("interpolates string, number, and boolean arguments", (): void => { + expect(format("{0} has {1} items, active: {2}", "Cart", 5, true)).toBe("Cart has 5 items, active: true"); + }); + + test("interpolates object and array arguments with dot-notation", (): void => { + expect(format("{0.name} bought {1.0} and {1.1}", { name: "Alice" }, ["Apple", "Banana"])).toBe( + "Alice bought Apple and Banana" + ); + }); + }); + + describe("no-placeholder templates", (): void => { + test("returns the template string as-is", (): void => { + expect(format("plain text")).toBe("plain text"); + }); + + test("returns the template as-is even when arguments are provided", (): void => { + expect(format("no placeholders", "arg1", "arg2")).toBe("no placeholders"); + }); + + test("returns the template as-is for an empty-arguments call", (): void => { + expect(format("hello")).toBe("hello"); + }); + }); + + describe("missing or out-of-range indices", (): void => { + test("returns 'undefined' for a missing argument index", (): void => { + expect(format("{0} {1}", "only")).toBe("only undefined"); + }); + + test("returns 'undefined' for an out-of-range index", (): void => { + expect(format("{5}", "a", "b")).toBe("undefined"); + }); + + test("returns 'undefined' when root argument is null with a dot path", (): void => { + expect(format("{0.name}", null)).toBe("undefined"); + }); + + test("returns 'undefined' when root argument is undefined with a dot path", (): void => { + expect(format("{0.name}", undefined)).toBe("undefined"); + }); + }); + + describe("malformed placeholders", (): void => { + test("preserves braces that do not match the placeholder pattern", (): void => { + expect(format("{ 0 }", "value")).toBe("{ 0 }"); + expect(format("{}", "value")).toBe("{}"); + expect(format("{abc}", "value")).toBe("{abc}"); + }); + + test("preserves unclosed braces", (): void => { + expect(format("{0 is open", "value")).toBe("{0 is open"); + }); + + test("preserves placeholders with negative indices", (): void => { + expect(format("{-1}", "value")).toBe("{-1}"); + }); + + test("preserves placeholders with leading dot paths", (): void => { + expect(format("{.name}", "value")).toBe("{.name}"); + }); + + test("preserves placeholders with trailing dots", (): void => { + expect(format("{0.}", "value")).toBe("{0.}"); + }); + + test("preserves placeholders with consecutive dots", (): void => { + expect(format("{0..name}", "value")).toBe("{0..name}"); + }); + + test("replaces only the inner placeholder when braces are doubled", (): void => { + expect(format("{{0}}", "value")).toBe("{value}"); + }); + }); + + describe("prototype and inherited properties", (): void => { + test("resolves own properties", (): void => { + const obj: Record = { own: "value" }; + expect(format("{0.own}", obj)).toBe("value"); + }); + + test("resolves inherited properties from the prototype chain", (): void => { + const proto: Record = { inherited: "parent" }; + const obj: Record = Object.create(proto) as Record; + expect(format("{0.inherited}", obj)).toBe("parent"); + }); + + test("returns 'undefined' for function-valued properties", (): void => { + expect(format("{0.toString}", {})).toBe("undefined"); + }); + }); + + describe("Unicode and combining marks", (): void => { + test("handles CJK characters", (): void => { + expect(format("Hello {0}", "世界")).toBe("Hello 世界"); + }); + + test("handles Thai characters", (): void => { + expect(format("สวัสดี {0}", "โลก")).toBe("สวัสดี โลก"); + }); + + test("handles Arabic characters", (): void => { + expect(format("{0} {1}", "مرحبا", "بالعالم")).toBe("مرحبا بالعالم"); + }); + + test("handles combining marks", (): void => { + expect(format("cafe\u0301: {0}", "open")).toBe("cafe\u0301: open"); + }); + + test("handles emoji", (): void => { + expect(format("{0} {1}", "Hello", "🌍")).toBe("Hello 🌍"); + }); + }); + + describe("empty and nullish", (): void => { + test("returns an empty string", (): void => { + expect(format("")).toBe(""); + expect(format(null)).toBe(""); + expect(format(undefined)).toBe(""); + }); + }); +}); + +describe("createFormatter", (): void => { + describe("valid template", (): void => { + test("returns a reusable function that formats arguments", (): void => { + const greet: (...args: unknown[]) => string = createFormatter("Hello {0}, welcome to {1}!"); + expect(greet("Alice", "Wonderland")).toBe("Hello Alice, welcome to Wonderland!"); + expect(greet("Bob", "the Matrix")).toBe("Hello Bob, welcome to the Matrix!"); + }); + + test("supports dot-notation in the pre-bound template", (): void => { + const fmt: (...args: unknown[]) => string = createFormatter("{0.name} is {0.age} years old"); + expect(fmt({ name: "Alice", age: 30 })).toBe("Alice is 30 years old"); + expect(fmt({ name: "Bob", age: 25 })).toBe("Bob is 25 years old"); + }); + + test("returns the template as-is when the formatter is called with no arguments", (): void => { + const fmt: (...args: unknown[]) => string = createFormatter("Hello {0}!"); + expect(fmt()).toBe("Hello undefined!"); + }); + }); + + describe("no-placeholder template", (): void => { + test("returns the template as-is on every call", (): void => { + const fmt: (...args: unknown[]) => string = createFormatter("static text"); + expect(fmt()).toBe("static text"); + expect(fmt("ignored")).toBe("static text"); + }); + }); + + describe("empty and nullish", (): void => { + test("returns a function that always returns an empty string", (): void => { + const fmtEmpty: (...args: unknown[]) => string = createFormatter(""); + expect(fmtEmpty("anything")).toBe(""); + + const fmtNull: (...args: unknown[]) => string = createFormatter(null); + expect(fmtNull("anything")).toBe(""); + + const fmtUndefined: (...args: unknown[]) => string = createFormatter(undefined); + expect(fmtUndefined("anything")).toBe(""); + }); + }); +}); diff --git a/src/libs/format/format.lib.ts b/src/libs/format/format.lib.ts new file mode 100644 index 0000000..7d117c6 --- /dev/null +++ b/src/libs/format/format.lib.ts @@ -0,0 +1,187 @@ +import { DOT, EMPTY, FORMAT_PLACEHOLDER_REGEX, LEFT_BRACE } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * A reusable formatting function returned by {@link createFormatter}. + */ +export type FormatFunction = (...args: unknown[]) => string; + +interface CompiledSegment { + /** Text preceding the placeholder. */ + readonly leadingText: string; + + /** Zero-based argument index. */ + readonly argIndex: number; + + /** Property path after the argument index. */ + readonly pathSegments: readonly string[]; +} + +interface CompiledTemplate { + /** Parsed placeholders and their preceding text. */ + readonly segments: readonly CompiledSegment[]; + + /** Text following the last placeholder. */ + readonly suffix: string; +} + +/** + * Resolves a property path against an object or array. + */ +function resolvePath(value: unknown, segments: readonly string[]): unknown { + const segmentCount: number = segments.length; + let current: unknown = value; + + for (let segmentIndex: number = 0; segmentIndex < segmentCount; segmentIndex++) { + if (current === null || current === undefined) { + return undefined; + } + + current = (current as Record)[segments[segmentIndex]]; + } + + return current; +} + +/** + * Converts a value to its formatted string representation. + * + * Objects and arrays are serialized with `JSON.stringify()`. Unsupported values or serialization errors resolve to `"undefined"`. + */ +function resolveValue(value: unknown): string { + if (typeof value === "string") { + return value; + } + + if (value === null) { + return "null"; + } + + if (value === undefined) { + return "undefined"; + } + + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + + try { + return JSON.stringify(value) ?? "undefined"; + } catch { + return "undefined"; + } +} + +/** + * Compiles a format template into reusable placeholder segments. + */ +function compileTemplate(template: string): CompiledTemplate { + const segments: CompiledSegment[] = []; + + let lastIndex: number = 0; + + for (const match of template.matchAll(FORMAT_PLACEHOLDER_REGEX)) { + const leadingText: string = template.slice(lastIndex, match.index); + const pathParts: string[] = match[1].split(DOT); + const argIndex: number = Number(pathParts[0]); + const pathSegments: string[] = pathParts.slice(1); + + segments.push({ leadingText, argIndex, pathSegments }); + + lastIndex = match.index + match[0].length; + } + + return { segments, suffix: template.slice(lastIndex) }; +} + +/** + * Formats a compiled template with the provided arguments. + */ +function formatCompiled(compiled: CompiledTemplate, args: readonly unknown[]): string { + const { segments, suffix }: CompiledTemplate = compiled; + const segmentCount: number = segments.length; + + if (segmentCount === 0) { + return suffix; + } + + let result: string = EMPTY; + + for (let segmentIndex: number = 0; segmentIndex < segmentCount; segmentIndex++) { + const segment: CompiledSegment = segments[segmentIndex]; + + result += segment.leadingText; + + const argValue: unknown = args[segment.argIndex]; + + if (segment.pathSegments.length === 0) { + result += resolveValue(argValue); + } else { + result += resolveValue(resolvePath(argValue, segment.pathSegments)); + } + } + + return result + suffix; +} + +/** + * Formats a template by replacing indexed placeholders with argument values. + * + * Supports `{index}` placeholders and dot notation for nested properties, + * such as `{0.name}` and `{0.items.0}`. + * + * @param template - The template to format. + * @param args - The values used to resolve placeholders. + * @returns The formatted string, or an empty string when `template` is empty, `null`, or `undefined`. + * + * @example + * + * format("Hello {0}!", "World"); // "Hello World!" + * format("Hello {0.name}!", { name: "Alice" }); // "Hello Alice!" + * format("{0.users.0.name}", { users: [{ name: "Bob" }] }); // "Bob" + */ +export function format(template: string | null | undefined, ...args: unknown[]): string { + if (!hasLength(template)) { + return EMPTY; + } + + if (!template.includes(LEFT_BRACE)) { + return template; + } + + const compiled: CompiledTemplate = compileTemplate(template); + + return formatCompiled(compiled, args); +} + +/** + * Creates a reusable formatter from a template. + * + * The template is compiled once when the formatter is created, making it efficient for repeated formatting with different arguments. + * + * @param template - The template to compile. + * @returns A reusable formatter function. + * + * @example + * const greet = createFormatter("Hello {0}!"); + * + * greet("Alice"); // "Hello Alice!" + * greet("Bob"); // "Hello Bob!" + * greet(""); // "Hello !" + * greet(null); // "Hello null!" + * greet(undefined); // "Hello undefined!" + */ +export function createFormatter(template: string | null | undefined): FormatFunction { + if (!hasLength(template)) { + return (): string => EMPTY; + } + + if (!template.includes(LEFT_BRACE)) { + return (): string => template; + } + + const compiled: CompiledTemplate = compileTemplate(template); + + return (...args: unknown[]): string => formatCompiled(compiled, args); +} diff --git a/src/libs/format/index.ts b/src/libs/format/index.ts new file mode 100644 index 0000000..5fa2556 --- /dev/null +++ b/src/libs/format/index.ts @@ -0,0 +1 @@ +export * from "./format.lib"; diff --git a/src/libs/has-length/has-length.lib.bench.ts b/src/libs/has-length/has-length.lib.bench.ts new file mode 100644 index 0000000..274022b --- /dev/null +++ b/src/libs/has-length/has-length.lib.bench.ts @@ -0,0 +1,89 @@ +import { describe, test, TestContext } from "vitest"; + +import { hasLength } from "./has-length.lib"; + +const shortText: string = "hello"; +const longText1KB: string = "a".repeat(1_000); +const longText100KB: string = "a".repeat(100_000); +const whitespaceString: string = " ".repeat(1_000); +const unicodeString: string = "你好世界 مرحبا สวัสดี こんにちは café"; + +describe("hasLength", (): void => { + test("given common short inputs", ({ bench }: TestContext): void => { + bench("non-empty string", (): void => { + hasLength(shortText); + }); + + bench("single-character string", (): void => { + hasLength("a"); + }); + + bench("whitespace-only string", (): void => { + hasLength(" "); + }); + }); + + test("given strings of different sizes", ({ bench }: TestContext): void => { + bench("1 KB string", (): void => { + hasLength(longText1KB); + }); + + bench("100 KB string", (): void => { + hasLength(longText100KB); + }); + + bench("long whitespace-only string", (): void => { + hasLength(whitespaceString); + }); + }); + + test("given Unicode strings", ({ bench }: TestContext): void => { + bench("Unicode string", (): void => { + hasLength(unicodeString); + }); + + bench("emoji string", (): void => { + hasLength("🎉🚀🌟"); + }); + + bench("combining-mark string", (): void => { + hasLength("cafe\u0301"); + }); + }); + + test("given non-string values", ({ bench }: TestContext): void => { + bench("number", (): void => { + hasLength(12345); + }); + + bench("boolean", (): void => { + hasLength(true); + }); + + bench("object with length property", (): void => { + hasLength({ length: 5 }); + }); + + bench("array", (): void => { + hasLength(["a", "b", "c"]); + }); + + bench("function", (): void => { + hasLength(() => {}); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + hasLength(""); + }); + + bench("null", (): void => { + hasLength(null); + }); + + bench("undefined", (): void => { + hasLength(undefined); + }); + }); +}); diff --git a/src/libs/has-length/has-length.lib.spec.ts b/src/libs/has-length/has-length.lib.spec.ts new file mode 100644 index 0000000..4131b0f --- /dev/null +++ b/src/libs/has-length/has-length.lib.spec.ts @@ -0,0 +1,118 @@ +import { describe, expect, expectTypeOf, test } from "vitest"; + +import { hasLength } from "./has-length.lib"; + +describe("hasLength", (): void => { + describe("given a non-empty string", (): void => { + test("returns true for standard text", (): void => { + expect(hasLength("Hello World")).toBe(true); + expect(hasLength("12345")).toBe(true); + }); + + test("returns true for a single code unit", (): void => { + expect(hasLength("a")).toBe(true); + expect(hasLength(".")).toBe(true); + expect(hasLength(" ")).toBe(true); + }); + + test("returns true for whitespace-only strings", (): void => { + expect(hasLength(" ")).toBe(true); + expect(hasLength("\t\n\r\v\f")).toBe(true); + }); + + test("returns true for symbols and emojis", (): void => { + expect(hasLength("!@#$%^&*()")).toBe(true); + expect(hasLength("🔥")).toBe(true); + expect(hasLength("🎉🚀🌟")).toBe(true); + }); + + test("returns true for Unicode and accented strings", (): void => { + expect(hasLength("สวัสดีชาวโลก")).toBe(true); + expect(hasLength("こんにちは")).toBe(true); + expect(hasLength("你好世界")).toBe(true); + expect(hasLength("مرحبا بالعالم")).toBe(true); + expect(hasLength("café")).toBe(true); + expect(hasLength("cafe\u0301")).toBe(true); + }); + }); + + describe("given a string with different lengths", (): void => { + test("returns true regardless of string length", (): void => { + expect(hasLength("a")).toBe(true); + expect(hasLength("ab")).toBe(true); + expect(hasLength("a".repeat(1000))).toBe(true); + expect(hasLength("a".repeat(100_000))).toBe(true); + }); + }); + + describe("given a non-string value", (): void => { + test("returns false for numbers", (): void => { + expect(hasLength(0)).toBe(false); + expect(hasLength(123)).toBe(false); + expect(hasLength(-456)).toBe(false); + expect(hasLength(Infinity)).toBe(false); + expect(hasLength(NaN)).toBe(false); + }); + + test("returns false for booleans", (): void => { + expect(hasLength(true)).toBe(false); + expect(hasLength(false)).toBe(false); + }); + + test("returns false for bigint and symbols", (): void => { + expect(hasLength(100n)).toBe(false); + expect(hasLength(Symbol("text"))).toBe(false); + }); + + test("returns false for objects with a length property", (): void => { + expect(hasLength({ length: 0 })).toBe(false); + expect(hasLength({ length: 5 })).toBe(false); + expect(hasLength({ value: "hello", length: 5 })).toBe(false); + }); + + test("returns false for arrays", (): void => { + expect(hasLength([])).toBe(false); + expect(hasLength(["hello"])).toBe(false); + expect(hasLength([1, 2, 3])).toBe(false); + }); + + test("returns false for built-in object instances", (): void => { + expect(hasLength(new Date())).toBe(false); + expect(hasLength(/hello/)).toBe(false); + }); + + test("returns false for functions regardless of their parameter count", (): void => { + const functionWithParameters: (_first: unknown, _second: unknown) => void = ( + _first: unknown, + _second: unknown + ): void => {}; + + expect(hasLength(() => {})).toBe(false); + expect(hasLength(functionWithParameters)).toBe(false); + }); + }); + + describe("given a type guard usage", (): void => { + test("narrows an unknown value to string", (): void => { + const value: unknown = "Hello"; + + if (hasLength(value)) { + expectTypeOf(value).toEqualTypeOf(); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns false for an empty string", (): void => { + expect(hasLength("")).toBe(false); + }); + + test("returns false for null", (): void => { + expect(hasLength(null)).toBe(false); + }); + + test("returns false for undefined", (): void => { + expect(hasLength(undefined)).toBe(false); + }); + }); +}); diff --git a/src/libs/has-length/has-length.lib.ts b/src/libs/has-length/has-length.lib.ts new file mode 100644 index 0000000..5c7a72c --- /dev/null +++ b/src/libs/has-length/has-length.lib.ts @@ -0,0 +1,18 @@ +/** + * Checks whether a value is a non-empty string. + * + * @param value - The value to check. + * @returns `true` if the value is a non-empty string; otherwise, `false`. + * + * @example + * + * hasLength("Hello World"); // true + * hasLength(" "); // true + * hasLength(""); // false + * hasLength(123); // false + * hasLength(null); // false + * hasLength(undefined); // false + */ +export function hasLength(value: unknown): value is string { + return typeof value === "string" && value.length > 0; +} diff --git a/src/libs/has-length/index.ts b/src/libs/has-length/index.ts new file mode 100644 index 0000000..1e80146 --- /dev/null +++ b/src/libs/has-length/index.ts @@ -0,0 +1 @@ +export * from "./has-length.lib"; diff --git a/src/libs/has-text/has-text.lib.bench.ts b/src/libs/has-text/has-text.lib.bench.ts new file mode 100644 index 0000000..48e8635 --- /dev/null +++ b/src/libs/has-text/has-text.lib.bench.ts @@ -0,0 +1,159 @@ +import { describe, test, TestContext } from "vitest"; + +import { hasText } from "./has-text.lib"; + +const shortText: string = "hello"; +const longText: string = "the quick brown fox jumps over the lazy dog ".repeat(100); +const longWhitespace: string = " ".repeat(1_000); +const longWhitespaceWithTextAtEnd: string = " ".repeat(1_000) + "a"; +const longTextWithTrailingWhitespace: string = "a" + " ".repeat(1_000); +const longText1KB: string = "a".repeat(1_000); +const longText100KB: string = "a".repeat(100_000); + +describe("hasText", (): void => { + test("given common short inputs", ({ bench }: TestContext): void => { + bench("non-whitespace text", (): void => { + hasText(shortText); + }); + + bench("single non-whitespace character", (): void => { + hasText("a"); + }); + + bench("single whitespace character", (): void => { + hasText(" "); + }); + + bench("whitespace-only string", (): void => { + hasText(" "); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("long text", (): void => { + hasText(longText); + }); + + bench("1 KB non-whitespace string", (): void => { + hasText(longText1KB); + }); + + bench("100 KB non-whitespace string", (): void => { + hasText(longText100KB); + }); + + bench("long whitespace-only string", (): void => { + hasText(longWhitespace); + }); + + bench("non-whitespace at the beginning", (): void => { + hasText(longTextWithTrailingWhitespace); + }); + + bench("non-whitespace at the end", (): void => { + hasText(longWhitespaceWithTextAtEnd); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("CJK", (): void => { + hasText("你好世界"); + }); + + bench("Arabic", (): void => { + hasText("مرحبا بالعالم"); + }); + + bench("Thai", (): void => { + hasText("สวัสดีชาวโลก"); + }); + + bench("accented Latin", (): void => { + hasText("café"); + }); + + bench("combining marks", (): void => { + hasText("cafe\u0301"); + }); + + bench("emoji", (): void => { + hasText("🎉🚀🌟"); + }); + }); + + test("given Unicode whitespace", ({ bench }: TestContext): void => { + bench("non-breaking spaces only", (): void => { + hasText("\u00A0\u00A0\u00A0"); + }); + + bench("ideographic spaces only", (): void => { + hasText("\u3000\u3000\u3000"); + }); + + bench("mixed Unicode whitespace only", (): void => { + hasText(" \t\n\r\u00A0\u2003\u202F\u3000 "); + }); + + bench("text surrounded by Unicode whitespace", (): void => { + hasText("\u00A0\u3000hello\u2003"); + }); + }); + + test("given symbols and emojis", ({ bench }: TestContext): void => { + bench("symbol-only string", (): void => { + hasText("!@#$%^&*()"); + }); + + bench("single emoji", (): void => { + hasText("🔥"); + }); + + bench("multiple emojis", (): void => { + hasText("🎉🚀🌟"); + }); + }); + + test("given non-string values", ({ bench }: TestContext): void => { + bench("number", (): void => { + hasText(12345); + }); + + bench("boolean", (): void => { + hasText(true); + }); + + bench("object", (): void => { + hasText({ length: 5 }); + }); + + bench("object with text property", (): void => { + hasText({ text: "hello" }); + }); + + bench("array", (): void => { + hasText(["hello"]); + }); + + bench("function", (): void => { + hasText(() => {}); + }); + + bench("symbol", (): void => { + hasText(Symbol("text")); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + hasText(""); + }); + + bench("null", (): void => { + hasText(null); + }); + + bench("undefined", (): void => { + hasText(undefined); + }); + }); +}); diff --git a/src/libs/has-text/has-text.lib.spec.ts b/src/libs/has-text/has-text.lib.spec.ts new file mode 100644 index 0000000..37eb3ea --- /dev/null +++ b/src/libs/has-text/has-text.lib.spec.ts @@ -0,0 +1,111 @@ +import { describe, expect, expectTypeOf, test } from "vitest"; + +import { hasText } from "./has-text.lib"; + +describe("hasText", (): void => { + describe("given a string containing text", (): void => { + test("returns true for standard text", (): void => { + expect(hasText("Hello World")).toBe(true); + expect(hasText("12345")).toBe(true); + }); + + test("returns true when text is surrounded by whitespace", (): void => { + expect(hasText(" Hello World ")).toBe(true); + expect(hasText("\t\n text \r\n")).toBe(true); + }); + + test("returns true for symbols and emojis", (): void => { + expect(hasText("!")).toBe(true); + expect(hasText("!@#$%^&*()")).toBe(true); + expect(hasText("🔥")).toBe(true); + expect(hasText("🎉🚀🌟")).toBe(true); + }); + + test("returns true for Unicode and accented text", (): void => { + expect(hasText("สวัสดีชาวโลก")).toBe(true); + expect(hasText("こんにちは")).toBe(true); + expect(hasText("你好世界")).toBe(true); + expect(hasText("مرحبا بالعالم")).toBe(true); + expect(hasText("café")).toBe(true); + expect(hasText("cafe\u0301")).toBe(true); + }); + }); + + describe("given a string containing only whitespace", (): void => { + test("returns false for ASCII whitespace", (): void => { + expect(hasText(" ")).toBe(false); + expect(hasText(" ")).toBe(false); + expect(hasText("\t\n\r\v\f")).toBe(false); + }); + + test("returns false for Unicode whitespace", (): void => { + expect(hasText("\u00A0")).toBe(false); + expect(hasText("\u2003")).toBe(false); + expect(hasText("\u202F")).toBe(false); + expect(hasText("\u3000")).toBe(false); + expect(hasText(" \t\n\r\u00A0\u2003\u202F\u3000 ")).toBe(false); + }); + }); + + describe("given a string containing combining marks", (): void => { + test("treats combining marks according to the configured whitespace rule", (): void => { + expect(hasText("cafe\u0301")).toBe(true); + }); + }); + + describe("given a non-string value", (): void => { + test("returns false for numbers", (): void => { + expect(hasText(0)).toBe(false); + expect(hasText(123)).toBe(false); + expect(hasText(-456)).toBe(false); + expect(hasText(Infinity)).toBe(false); + expect(hasText(NaN)).toBe(false); + }); + + test("returns false for booleans", (): void => { + expect(hasText(true)).toBe(false); + expect(hasText(false)).toBe(false); + }); + + test("returns false for bigint and symbols", (): void => { + expect(hasText(100n)).toBe(false); + expect(hasText(Symbol("text"))).toBe(false); + }); + + test("returns false for objects and arrays", (): void => { + expect(hasText({})).toBe(false); + expect(hasText({ text: "hello" })).toBe(false); + expect(hasText([])).toBe(false); + expect(hasText(["hello"])).toBe(false); + }); + + test("returns false for functions", (): void => { + expect(hasText(() => {})).toBe(false); + expect(hasText(function () {})).toBe(false); + }); + }); + + describe("given a type guard usage", (): void => { + test("narrows an unknown value to string", (): void => { + const value: unknown = "Hello"; + + if (hasText(value)) { + expectTypeOf(value).toEqualTypeOf(); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns false for an empty string", (): void => { + expect(hasText("")).toBe(false); + }); + + test("returns false for null", (): void => { + expect(hasText(null)).toBe(false); + }); + + test("returns false for undefined", (): void => { + expect(hasText(undefined)).toBe(false); + }); + }); +}); diff --git a/src/libs/has-text/has-text.lib.ts b/src/libs/has-text/has-text.lib.ts new file mode 100644 index 0000000..04efa6f --- /dev/null +++ b/src/libs/has-text/has-text.lib.ts @@ -0,0 +1,20 @@ +const NON_WHITESPACE_REGEX: RegExp = /\S/; + +/** + * Checks whether a value is a string containing at least one non-whitespace character. + * + * @param value - The value to check. + * @returns `true` if the value is a string containing at least one non-whitespace character; otherwise, `false`. + * + * @example + * + * hasText("Hello World"); // true + * hasText(" "); // false + * hasText(""); // false + * hasText(123); // false + * hasText(null); // false + * hasText(undefined); // false + */ +export function hasText(value: unknown): value is string { + return typeof value === "string" && NON_WHITESPACE_REGEX.test(value); +} diff --git a/src/libs/has-text/index.ts b/src/libs/has-text/index.ts new file mode 100644 index 0000000..eda1cbc --- /dev/null +++ b/src/libs/has-text/index.ts @@ -0,0 +1 @@ +export * from "./has-text.lib"; diff --git a/src/libs/index.ts b/src/libs/index.ts new file mode 100644 index 0000000..b89652f --- /dev/null +++ b/src/libs/index.ts @@ -0,0 +1,16 @@ +export * from "./camel-case"; +export * from "./capitalize"; +export * from "./collapse-whitespace"; +export * from "./constant-case"; +export * from "./default-if-blank"; +export * from "./default-if-empty"; +export * from "./format"; +export * from "./has-length"; +export * from "./has-text"; +export * from "./kebab-case"; +export * from "./mask"; +export * from "./pascal-case"; +export * from "./redact"; +export * from "./slugify"; +export * from "./snake-case"; +export * from "./uncapitalize"; diff --git a/src/libs/kebab-case/index.ts b/src/libs/kebab-case/index.ts new file mode 100644 index 0000000..78189db --- /dev/null +++ b/src/libs/kebab-case/index.ts @@ -0,0 +1 @@ +export * from "./kebab-case.lib"; diff --git a/src/libs/kebab-case/kebab-case.lib.bench.ts b/src/libs/kebab-case/kebab-case.lib.bench.ts new file mode 100644 index 0000000..07242fb --- /dev/null +++ b/src/libs/kebab-case/kebab-case.lib.bench.ts @@ -0,0 +1,184 @@ +import { describe, test, TestContext } from "vitest"; + +import { kebabCase } from "./kebab-case.lib"; + +const longSlug: string = "get-HTTP-response-code-version-2.0-API ".repeat(100); +const longPascalCase: string = "XMLHttpRequestResponseCodeHandler".repeat(100); +const longCamelCase: string = "xmlHttpRequestResponseCodeHandler".repeat(100); +const longAllCaps: string = "MAX_RETRY_COUNT_FOR_HTTP_500_ERRORS ".repeat(100); +const longUnicode: string = "Привет-Мир café-noir 你好-世界 こんにちは-世界 مرحبا-بالعالم ".repeat(100); +const longSeparators: string = "---foo---bar___baz...qux!!!".repeat(100); +const longWhitespace: string = " ".repeat(1_000); +const longSymbols: string = "!@#$%^&*()_+-=[]{}|;:',.<>?/`~".repeat(100); + +describe("kebabCase", (): void => { + test("given common short inputs", ({ bench }: TestContext): void => { + bench("hyphen-separated lowercase", (): void => { + kebabCase("foo-bar"); + }); + + bench("underscore-separated uppercase", (): void => { + kebabCase("FOO_BAR"); + }); + + bench("camelCase", (): void => { + kebabCase("fooBar"); + }); + + bench("PascalCase", (): void => { + kebabCase("FooBar"); + }); + + bench("single word", (): void => { + kebabCase("hello"); + }); + + bench("single character", (): void => { + kebabCase("A"); + }); + }); + + test("given numbers and acronyms", ({ bench }: TestContext): void => { + bench("numeric-only input", (): void => { + kebabCase("123456789"); + }); + + bench("word with numbers", (): void => { + kebabCase("version-2-0"); + }); + + bench("number followed by word", (): void => { + kebabCase("2foo"); + }); + + bench("word followed by number", (): void => { + kebabCase("foo2bar"); + }); + + bench("acronym followed by word", (): void => { + kebabCase("HTTPResponse"); + }); + + bench("word followed by acronym", (): void => { + kebabCase("parseHTTP"); + }); + + bench("multiple acronyms", (): void => { + kebabCase("HTTPJSONAPIResponse"); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("long slug with acronyms and numbers", (): void => { + kebabCase(longSlug); + }); + + bench("long PascalCase with acronyms", (): void => { + kebabCase(longPascalCase); + }); + + bench("long camelCase with acronyms", (): void => { + kebabCase(longCamelCase); + }); + + bench("long ALL_CAPS with numbers", (): void => { + kebabCase(longAllCaps); + }); + + bench("long Unicode input", (): void => { + kebabCase(longUnicode); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("accented Latin", (): void => { + kebabCase("crème-brûlée"); + }); + + bench("combining marks", (): void => { + kebabCase("cafe\u0301 noir"); + }); + + bench("Cyrillic", (): void => { + kebabCase("Привет Мир"); + }); + + bench("CJK", (): void => { + kebabCase("你好 世界"); + }); + + bench("Hiragana", (): void => { + kebabCase("こんにちは 世界"); + }); + + bench("Hangul", (): void => { + kebabCase("안녕하세요 세계"); + }); + + bench("Arabic", (): void => { + kebabCase("مرحبا بالعالم"); + }); + + bench("Thai", (): void => { + kebabCase("สวัสดี โลก"); + }); + + bench("mixed scripts", (): void => { + kebabCase("hello 世界 Привет"); + }); + }); + + test("given excessive separators", ({ bench }: TestContext): void => { + bench("leading and trailing separators", (): void => { + kebabCase("---foo---bar---"); + }); + + bench("repeated symbols", (): void => { + kebabCase("...___foo!!!bar???baz..."); + }); + + bench("repeated whitespace", (): void => { + kebabCase(" foo bar baz "); + }); + + bench("mixed separators", (): void => { + kebabCase(" -- foo __ bar .. baz -- "); + }); + + bench("long separator-heavy input", (): void => { + kebabCase(longSeparators); + }); + }); + + test("given non-extractable input", ({ bench }: TestContext): void => { + bench("symbol-only input", (): void => { + kebabCase("!@#$%^&*()_+-=[]{}"); + }); + + bench("long symbol-only input", (): void => { + kebabCase(longSymbols); + }); + + bench("separator-only input", (): void => { + kebabCase("---___..."); + }); + + bench("long whitespace-only input", (): void => { + kebabCase(longWhitespace); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + kebabCase(""); + }); + + bench("null", (): void => { + kebabCase(null); + }); + + bench("undefined", (): void => { + kebabCase(undefined); + }); + }); +}); diff --git a/src/libs/kebab-case/kebab-case.lib.spec.ts b/src/libs/kebab-case/kebab-case.lib.spec.ts new file mode 100644 index 0000000..aff893c --- /dev/null +++ b/src/libs/kebab-case/kebab-case.lib.spec.ts @@ -0,0 +1,193 @@ +import { describe, expect, test } from "vitest"; + +import { kebabCase } from "./kebab-case.lib"; + +describe("kebabCase", (): void => { + describe("given separated words", (): void => { + test("converts common separators to hyphens", (): void => { + expect(kebabCase("foo-bar")).toBe("foo-bar"); + expect(kebabCase("foo_bar")).toBe("foo-bar"); + expect(kebabCase("foo.bar")).toBe("foo-bar"); + expect(kebabCase("foo bar")).toBe("foo-bar"); + }); + + test("removes leading and trailing separators", (): void => { + expect(kebabCase("--foo-bar--")).toBe("foo-bar"); + expect(kebabCase("__foo_bar__")).toBe("foo-bar"); + expect(kebabCase("..foo.bar..")).toBe("foo-bar"); + expect(kebabCase(" foo bar ")).toBe("foo-bar"); + }); + + test("collapses mixed separators between words", (): void => { + expect(kebabCase("foo-bar_baz.qux")).toBe("foo-bar-baz-qux"); + expect(kebabCase("foo / bar \\ baz")).toBe("foo-bar-baz"); + expect(kebabCase("foo!!!bar???baz")).toBe("foo-bar-baz"); + }); + }); + + describe("given different casing styles", (): void => { + test("normalizes lowercase and uppercase words", (): void => { + expect(kebabCase("foo-bar")).toBe("foo-bar"); + expect(kebabCase("FOO-BAR")).toBe("foo-bar"); + expect(kebabCase("FOO_BAR_BAZ")).toBe("foo-bar-baz"); + }); + + test("converts camelCase to kebab-case", (): void => { + expect(kebabCase("fooBar")).toBe("foo-bar"); + expect(kebabCase("fooBarBaz")).toBe("foo-bar-baz"); + expect(kebabCase("xmlHttpRequest")).toBe("xml-http-request"); + }); + + test("converts PascalCase to kebab-case", (): void => { + expect(kebabCase("FooBar")).toBe("foo-bar"); + expect(kebabCase("FooBarBaz")).toBe("foo-bar-baz"); + expect(kebabCase("XmlHttpRequest")).toBe("xml-http-request"); + }); + }); + + describe("given acronyms", (): void => { + test("splits acronym boundaries correctly", (): void => { + expect(kebabCase("HTTPResponseCode")).toBe("http-response-code"); + expect(kebabCase("JSONDataAPI")).toBe("json-data-api"); + expect(kebabCase("XMLHttpRequest")).toBe("xml-http-request"); + }); + + test("handles acronyms at the beginning and end", (): void => { + expect(kebabCase("HTTPResponse")).toBe("http-response"); + expect(kebabCase("parseHTTP")).toBe("parse-http"); + expect(kebabCase("parseHTTPResponse")).toBe("parse-http-response"); + expect(kebabCase("getURLValue")).toBe("get-url-value"); + }); + + test("handles multiple acronyms separated by delimiters", (): void => { + expect(kebabCase("HTTP-JSON-API-Response")).toBe("http-json-api-response"); + }); + }); + + describe("given numbers", (): void => { + test("separates numbers from adjacent words", (): void => { + expect(kebabCase("2foo")).toBe("2-foo"); + expect(kebabCase("foo2")).toBe("foo-2"); + expect(kebabCase("foo2bar")).toBe("foo-2-bar"); + }); + + test("preserves numeric words", (): void => { + expect(kebabCase("123")).toBe("123"); + expect(kebabCase("123-456")).toBe("123-456"); + expect(kebabCase("123_456_789")).toBe("123-456-789"); + }); + + test("handles decimal-like input as separate words", (): void => { + expect(kebabCase("version 2.0")).toBe("version-2-0"); + expect(kebabCase("version-2-0")).toBe("version-2-0"); + }); + }); + + describe("given Unicode text", (): void => { + test("preserves accented Latin characters", (): void => { + expect(kebabCase("crème-brûlée")).toBe("crème-brûlée"); + expect(kebabCase("jalapeño popper")).toBe("jalapeño-popper"); + expect(kebabCase("École Normale")).toBe("école-normale"); + }); + + test("handles Cyrillic casing", (): void => { + expect(kebabCase("Привет Мир")).toBe("привет-мир"); + expect(kebabCase("привет-мир")).toBe("привет-мир"); + expect(kebabCase("ПРИВЕТ МИР")).toBe("привет-мир"); + }); + + test("preserves combining marks", (): void => { + expect(kebabCase("cafe\u0301 noir")).toBe("cafe\u0301-noir"); + expect(kebabCase("Cafe\u0301 Noir")).toBe("cafe\u0301-noir"); + }); + }); + + describe("given non-cased scripts", (): void => { + test("separates CJK words", (): void => { + expect(kebabCase("你好 世界")).toBe("你好-世界"); + }); + + test("separates Hiragana and Hangul words", (): void => { + expect(kebabCase("こんにちは 世界")).toBe("こんにちは-世界"); + expect(kebabCase("안녕하세요 세계")).toBe("안녕하세요-세계"); + }); + + test("separates Arabic words", (): void => { + expect(kebabCase("مرحبا بالعالم")).toBe("مرحبا-بالعالم"); + }); + + test("handles mixed scripts", (): void => { + expect(kebabCase("hello 世界")).toBe("hello-世界"); + expect(kebabCase("Hello 世界")).toBe("hello-世界"); + expect(kebabCase("foo-こんにちは")).toBe("foo-こんにちは"); + expect(kebabCase("hello 世界 Привет")).toBe("hello-世界-привет"); + }); + }); + + describe("given symbols and separators", (): void => { + test("ignores symbols between words", (): void => { + expect(kebabCase("foo!!!bar")).toBe("foo-bar"); + expect(kebabCase("foo---bar")).toBe("foo-bar"); + expect(kebabCase("foo...bar")).toBe("foo-bar"); + expect(kebabCase("foo___bar")).toBe("foo-bar"); + }); + + test("returns an empty string when no words are extractable", (): void => { + expect(kebabCase("---")).toBe(""); + expect(kebabCase("___")).toBe(""); + expect(kebabCase("...")).toBe(""); + expect(kebabCase("!!!")).toBe(""); + expect(kebabCase("@#$%^&*")).toBe(""); + }); + }); + + describe("given a single word", (): void => { + test("returns the normalized word without adding separators", (): void => { + expect(kebabCase("hello")).toBe("hello"); + expect(kebabCase("Hello")).toBe("hello"); + expect(kebabCase("HELLO")).toBe("hello"); + }); + + test("preserves a numeric-only word", (): void => { + expect(kebabCase("123456789")).toBe("123456789"); + }); + }); + + describe("given a kebab-case result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "foo-bar", + "FOO_BAR", + "fooBar", + "FooBar", + "HTTPResponseCode", + "JSONDataAPI", + "version-2-0", + "crème-brûlée", + "Привет Мир", + "hello 世界" + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = kebabCase(input); + + expect(kebabCase(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(kebabCase("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(kebabCase(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(kebabCase(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/kebab-case/kebab-case.lib.ts b/src/libs/kebab-case/kebab-case.lib.ts new file mode 100644 index 0000000..6c66d53 --- /dev/null +++ b/src/libs/kebab-case/kebab-case.lib.ts @@ -0,0 +1,48 @@ +import { EMPTY, HYPHEN, WORD_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Converts a string to kebab-case. + * + * Handles separators, existing casing styles, acronyms, numbers, Unicode characters, and combining marks. + * + * @param value - The string to convert to kebab-case, or `null` or `undefined`. + * @returns The kebab-case string, or an empty string when `value` is empty, `null`, or `undefined`, or contains no extractable words. + * + * @example + * + * kebabCase("Foo Bar"); // "foo-bar" + * kebabCase("--foo-bar--"); // "foo-bar" + * kebabCase("FOO_BAR"); // "foo-bar" + * kebabCase("fooBar"); // "foo-bar" + * kebabCase("XMLHttpRequest"); // "xml-http-request" + * kebabCase("version 2.0"); // "version-2-0" + * kebabCase("Привет Мир"); // "привет-мир" + * kebabCase("你好 世界"); // "你好-世界" + * kebabCase("cafe\u0301 noir"); // "cafe\u0301-noir" + * kebabCase(""); // "" + * kebabCase(null); // "" + * kebabCase(undefined); // "" + */ +export function kebabCase(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const words: RegExpMatchArray | null = value.match(WORD_REGEX); + + if (!words) { + return EMPTY; + } + + const length: number = words.length; + + let result: string = words[0].toLowerCase(); + + for (let index: number = 1; index < length; index++) { + result += HYPHEN + words[index].toLowerCase(); + } + + return result; +} diff --git a/src/libs/mask/index.ts b/src/libs/mask/index.ts new file mode 100644 index 0000000..2ed2b49 --- /dev/null +++ b/src/libs/mask/index.ts @@ -0,0 +1 @@ +export * from "./mask.lib"; diff --git a/src/libs/mask/mask.lib.bench.ts b/src/libs/mask/mask.lib.bench.ts new file mode 100644 index 0000000..15f4f5b --- /dev/null +++ b/src/libs/mask/mask.lib.bench.ts @@ -0,0 +1,173 @@ +import { describe, test, TestContext } from "vitest"; + +import { createMask, mask } from "./mask.lib"; + +const compiledPhoneMask: (value: string | null | undefined) => string = createMask("(###) ###-####"); +const compiledFlexibleMask: (value: string | null | undefined) => string = createMask("(###) ###-####", { + fallbackPatterns: ["(##) ###-####"] +}); +const compiledUnicodeMask: (value: string | null | undefined) => string = createMask("*-*-*-*-*-*", { + mode: "grapheme" +}); + +const longDigits: string = "0812345678".repeat(100); +const longMixedInput: string = "ABC1234XYZ5678".repeat(50); +const longSeparators: string = "---081---234---5678---".repeat(50); + +describe("mask", (): void => { + test("short ASCII", ({ bench }: TestContext): void => { + bench("phone number mask", (): void => { + mask("0812345678", "(###) ###-####"); + }); + + bench("credit card mask", (): void => { + mask("1234567890123456", "#### #### #### ####"); + }); + + bench("postal code mask with letters and digits", (): void => { + mask("K9V1B2", "A#A #A#"); + }); + }); + + test("long input", ({ bench }: TestContext): void => { + bench("long digits non-matching", (): void => { + mask(longDigits, "(###) ###-####"); + }); + + bench("long mixed alphanumeric non-matching", (): void => { + mask(longMixedInput, "AAA-####-AAA-####"); + }); + + bench("long separators non-matching", (): void => { + mask(longSeparators, "(###) ###-####"); + }); + + bench("long serial alphanumeric code", (): void => { + mask("HTTP20API1234567890XYZ9999ABCD", "AAAA-##-AAA-####-####-####-AAA-####"); + }); + }); + + test("Unicode and combining marks", ({ bench }: TestContext): void => { + bench("Thai letters and digits", (): void => { + mask("กขค123456", "AAA-###-###"); + }); + + bench("combining marks with grapheme mode", (): void => { + mask("cafe\u0301123456", "AAAA-###-###", { mode: "grapheme" }); + }); + + bench("emojis with codePoint mode", (): void => { + mask("😀😀123456", "**-###-###", { mode: "codePoint" }); + }); + }); + + test("excessive separators", ({ bench }: TestContext): void => { + bench("input heavily padded with separators and hyphens", (): void => { + mask("---081---234---5678---", "(###) ###-####"); + }); + }); + + test("3-mode comparison", ({ bench }: TestContext): void => { + bench("ASCII [utf16]", (): void => { + mask("0812345678", "(###) ###-####", { mode: "utf16" }); + }); + + bench("ASCII [codePoint]", (): void => { + mask("0812345678", "(###) ###-####", { mode: "codePoint" }); + }); + + bench("ASCII [grapheme]", (): void => { + mask("0812345678", "(###) ###-####", { mode: "grapheme" }); + }); + + bench("Emoji [utf16]", (): void => { + mask("😀😀😀😀", "**-**", { mode: "utf16" }); + }); + + bench("Emoji [codePoint]", (): void => { + mask("😀😀😀😀", "**-**", { mode: "codePoint" }); + }); + + bench("Emoji [grapheme]", (): void => { + mask("😀😀😀😀", "**-**", { mode: "grapheme" }); + }); + }); + + test("empty and nullish", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + mask("", "(###) ###-####"); + }); + + bench("null", (): void => { + mask(null, "(###) ###-####"); + }); + + bench("undefined", (): void => { + mask(undefined, "(###) ###-####"); + }); + }); +}); + +describe("createMask", (): void => { + test("compile overhead", ({ bench }: TestContext): void => { + bench("simple pattern", (): void => { + createMask("(###) ###-####"); + }); + + bench("alphanumeric pattern", (): void => { + createMask("AAA-####-AAA-####"); + }); + + bench("pattern with fallbackPatterns", (): void => { + createMask("(###) ###-####", { fallbackPatterns: ["(##) ###-####"] }); + }); + + bench("grapheme mode pattern", (): void => { + createMask("*-*-*-*-*-*", { mode: "grapheme" }); + }); + }); + + test("pre-compiled reuse", ({ bench }: TestContext): void => { + bench("phone number call", (): void => { + compiledPhoneMask("0812345678"); + }); + + bench("fallbackPatterns call", (): void => { + compiledFlexibleMask("021234567"); + }); + + bench("grapheme mode call", (): void => { + compiledUnicodeMask("👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦"); + }); + }); +}); + +describe("mask vs createMask comparison", (): void => { + test("phone number pattern", async ({ bench }: TestContext): Promise => { + const pattern: string = "(###) ###-####"; + const compiled: (value: string | null | undefined) => string = createMask(pattern); + + await bench.compare( + bench("mask() one-shot", (): void => { + mask("0812345678", pattern); + }), + bench("createMask() pre-compiled", (): void => { + compiled("0812345678"); + }) + ); + }); + + test("grapheme mode pattern", async ({ bench }: TestContext): Promise => { + const pattern: string = "*-*-*-*-*-*"; + const compiled: (value: string | null | undefined) => string = createMask(pattern, { mode: "grapheme" }); + + await bench.compare( + bench("mask() grapheme one-shot", (): void => { + mask("👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦", pattern, { mode: "grapheme" }); + }), + bench("createMask() grapheme pre-compiled", (): void => { + compiled("👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦👨‍👩‍👧‍👦"); + }) + ); + }); +}); diff --git a/src/libs/mask/mask.lib.spec.ts b/src/libs/mask/mask.lib.spec.ts new file mode 100644 index 0000000..b1e2388 --- /dev/null +++ b/src/libs/mask/mask.lib.spec.ts @@ -0,0 +1,296 @@ +import { describe, expect, test } from "vitest"; + +import { createMask, mask } from "./mask.lib"; + +describe("mask", (): void => { + describe("digit patterns (#)", (): void => { + test("formats phone numbers correctly", (): void => { + expect(mask("0812345678", "(###) ###-####")).toBe("(081) 234-5678"); + expect(mask("0812345678", "###-###-####")).toBe("081-234-5678"); + expect(mask("1234567", "###-####")).toBe("123-4567"); + }); + + test("formats credit card numbers with spaces", (): void => { + expect(mask("1234567890123456", "#### #### #### ####")).toBe("1234 5678 9012 3456"); + }); + + test("formats date strings", (): void => { + expect(mask("20260822", "####-##-##")).toBe("2026-08-22"); + expect(mask("22082026", "##/##/####")).toBe("22/08/2026"); + }); + + test("formats strings with country code prefix literals", (): void => { + expect(mask("812345678", "+66 ##-###-####")).toBe("+66 81-234-5678"); + }); + + test("handles already partially formatted or separator-containing inputs", (): void => { + expect(mask("081-234-5678", "(###) ###-####")).toBe("(081) 234-5678"); + expect(mask("(081) 234-5678", "(###) ###-####")).toBe("(081) 234-5678"); + expect(mask("123-45-6789", "###-##-####")).toBe("123-45-6789"); + }); + }); + + describe("letter patterns (A)", (): void => { + test("formats letter-only masks", (): void => { + expect(mask("HELLOWORLD", "AAAAA-AAAAA")).toBe("HELLO-WORLD"); + expect(mask("abcdef", "AA-AA-AA")).toBe("ab-cd-ef"); + expect(mask("ABCD", "AA-AA")).toBe("AB-CD"); + expect(mask("---abc---def", "AAA-AAA")).toBe("abc-def"); + }); + }); + + describe("wildcard patterns (*)", (): void => { + test("accepts any character including digits, letters, and symbols", (): void => { + expect(mask("A1-B2", "**-**")).toBe("A1-B2"); + expect(mask("ABC123", "***-***")).toBe("ABC-123"); + expect(mask("ABC-123", "***-***")).toBe("ABC-123"); + expect(mask("X#9$", "****")).toBe("X#9$"); + }); + }); + + describe("mixed token patterns (#, A, *)", (): void => { + test("formats postal codes, vehicle plates, and serial numbers", (): void => { + expect(mask("K9V1B2", "A#A #A#")).toBe("K9V 1B2"); + expect(mask("ABC1234", "AAA-####")).toBe("ABC-1234"); + expect(mask("AB12CD34", "AA##-**##")).toBe("AB12-CD34"); + }); + }); + + describe("non-matching input", (): void => { + test("returns original value when input does not match pattern", (): void => { + expect(mask("081", "(###) ###-####")).toBe("081"); + expect(mask("0812", "(###) ###-####")).toBe("0812"); + expect(mask("081234", "(###) ###-####")).toBe("081234"); + expect(mask("AB", "AAA-####")).toBe("AB"); + expect(mask("12345678909999", "###-###")).toBe("12345678909999"); + expect(mask("abc", "###-###")).toBe("abc"); + expect(mask("123", "AAA-AAA")).toBe("123"); + expect(mask("---", "AAA")).toBe("---"); + expect(mask("---", "###")).toBe("---"); + expect(mask("12-", "###")).toBe("12-"); + expect(mask("AB-", "AAA")).toBe("AB-"); + }); + }); + + describe("fallback patterns", (): void => { + test("selects exact slot count match when available in declaration order", (): void => { + const options: { fallbackPatterns: string[] } = { + fallbackPatterns: ["(##) ###-####"] + }; + + expect(mask("021234567", "(###) ###-####", options)).toBe("(02) 123-4567"); + expect(mask("0812345678", "(###) ###-####", options)).toBe("(081) 234-5678"); + }); + + test("chooses best matching pattern from fallback list in declaration order", (): void => { + const options: { fallbackPatterns: string[] } = { + fallbackPatterns: ["###-####", "###-###-####"] + }; + + expect(mask("1234567", "##-###-####", options)).toBe("123-4567"); + expect(mask("1234567890", "##-###-####", options)).toBe("123-456-7890"); + }); + + test("returns original value when neither primary nor fallback patterns match", (): void => { + const options: { fallbackPatterns: string[] } = { + fallbackPatterns: ["(##) ###-####"] + }; + + expect(mask("08123", "(###) ###-####", options)).toBe("08123"); + expect(mask("123456789012", "(###) ###-####", options)).toBe("123456789012"); + }); + + test("handles fallback patterns in codePoint mode", (): void => { + const options: { fallbackPatterns: string[]; mode: "codePoint" } = { + fallbackPatterns: ["**-###", "***-###"], + mode: "codePoint" + }; + + expect(mask("😀😀123", "####-###", options)).toBe("😀😀-123"); + expect(mask("😀😀😀123", "####-###", { fallbackPatterns: ["AA-###", "***-###"], mode: "codePoint" })).toBe( + "😀😀😀-123" + ); + expect(mask("12", "####-###", options)).toBe("12"); + }); + + test("handles fallback patterns in grapheme mode", (): void => { + const options: { fallbackPatterns: string[]; mode: "grapheme" } = { + fallbackPatterns: ["*-##", "*-###"], + mode: "grapheme" + }; + + expect(mask("👨‍👩‍👧‍👦12", "AAA-####", options)).toBe("👨‍👩‍👧‍👦-12"); + expect(mask("👨‍👩‍👧‍👦123", "AAA-####", options)).toBe("👨‍👩‍👧‍👦-123"); + expect(mask("1", "AAA-####", options)).toBe("1"); + }); + }); + + describe("Unicode and combining marks", (): void => { + test("handles Unicode letters and decimal digits across scripts", (): void => { + expect(mask("กขค123", "AAA-###")).toBe("กขค-123"); + expect(mask("สวัสดี", "AA-AAAA")).toBe("สว-ัสดี"); + expect(mask("๑๒๓๔๕๖", "###-###")).toBe("๑๒๓-๔๕๖"); + expect(mask("กขค123", "AAA-###", { mode: "utf16" })).toBe("กขค-123"); + expect(mask("กขค123", "AAA-###", { mode: "codePoint" })).toBe("กขค-123"); + expect(mask("กขค123", "AAA-###", { mode: "grapheme" })).toBe("กขค-123"); + }); + + test("treats surrogate pairs as 2 code units in utf16 mode", (): void => { + expect(mask("😀", "**", { mode: "utf16" })).toBe("😀"); + expect(mask("😀", "*", { mode: "utf16" })).toBe("😀"); + expect(mask("😀", "*", { mode: "codePoint" })).toBe("😀"); + expect(mask("😀", "*", { mode: "grapheme" })).toBe("😀"); + }); + + test("handles codePoint mode with emojis and surrogate pairs", (): void => { + expect(mask("😀😀123", "**-###", { mode: "codePoint" })).toBe("😀😀-123"); + expect(mask("🎉🎊🎈✨", "**-**", { mode: "codePoint" })).toBe("🎉🎊-🎈✨"); + expect(mask("😀", "**-**", { mode: "codePoint" })).toBe("😀"); + expect(mask("😀-😀", "*-*", { mode: "codePoint" })).toBe("😀-😀"); + expect(mask("😀😀-😀😀", "**-**", { mode: "codePoint" })).toBe("😀😀-😀😀"); + expect(mask("😀abc", "*-###", { mode: "codePoint" })).toBe("😀abc"); + expect(mask("123456", "###", { mode: "codePoint" })).toBe("123456"); + expect(mask("ABCDEF", "AAA", { mode: "codePoint" })).toBe("ABCDEF"); + expect(mask("😀😀", "AA", { mode: "codePoint" })).toBe("😀😀"); + expect(mask("😀😀", "##", { mode: "codePoint" })).toBe("😀😀"); + expect(mask("😀", "*-*", { mode: "codePoint" })).toBe("😀"); + }); + + test("handles grapheme mode with complex emojis and combining marks", (): void => { + expect(mask("👨‍👩‍👧‍👦123", "*-###", { mode: "grapheme" })).toBe("👨‍👩‍👧‍👦-123"); + expect(mask("cafe\u030112", "AAAA-##", { mode: "grapheme" })).toBe("cafe\u0301-12"); + expect(mask("cafe\u0301ABC", "AAAA-AAA", { mode: "grapheme" })).toBe("cafe\u0301-ABC"); + expect(mask("😀", "*-*", { mode: "grapheme" })).toBe("😀"); + expect(mask("😀-😀", "*-*", { mode: "grapheme" })).toBe("😀-😀"); + expect(mask("😀😀-😀😀", "**-**", { mode: "grapheme" })).toBe("😀😀-😀😀"); + expect(mask("😀abc", "*-###", { mode: "grapheme" })).toBe("😀abc"); + expect(mask("123456", "###", { mode: "grapheme" })).toBe("123456"); + expect(mask("ABCDEF", "AAA", { mode: "grapheme" })).toBe("ABCDEF"); + expect(mask("👨‍👩‍👧‍👦", "AA", { mode: "grapheme" })).toBe("👨‍👩‍👧‍👦"); + expect(mask("👨‍👩‍👧‍👦", "##", { mode: "grapheme" })).toBe("👨‍👩‍👧‍👦"); + expect(mask("👨‍👩‍👧‍👦", "*-*", { mode: "grapheme" })).toBe("👨‍👩‍👧‍👦"); + }); + }); + + describe("createMask factory", (): void => { + test("creates a reusable masking function with default options", (): void => { + const maskPhone: (value: string | null | undefined) => string = createMask("(###) ###-####"); + + expect(maskPhone("0812345678")).toBe("(081) 234-5678"); + expect(maskPhone("0899999999")).toBe("(089) 999-9999"); + expect(maskPhone("0812")).toBe("0812"); + expect(maskPhone("")).toBe(""); + }); + + test("creates a reusable masking function with fallbackPatterns in utf16, codePoint, and grapheme modes", (): void => { + const maskFlexiblePhone: (value: string | null | undefined) => string = createMask("(###) ###-####", { + fallbackPatterns: ["(##) ###-####"] + }); + + expect(maskFlexiblePhone("021234567")).toBe("(02) 123-4567"); + expect(maskFlexiblePhone("0812345678")).toBe("(081) 234-5678"); + expect(maskFlexiblePhone("0812")).toBe("0812"); + expect(maskFlexiblePhone("")).toBe(""); + + const maskCPWithFallbacks: (value: string | null | undefined) => string = createMask("####-###", { + fallbackPatterns: ["**-###"], + mode: "codePoint" + }); + + expect(maskCPWithFallbacks("😀😀123")).toBe("😀😀-123"); + expect(maskCPWithFallbacks("12")).toBe("12"); + expect(maskCPWithFallbacks("")).toBe(""); + + const maskGraphWithFallbacks: (value: string | null | undefined) => string = createMask("AAA-####", { + fallbackPatterns: ["*-##"], + mode: "grapheme" + }); + + expect(maskGraphWithFallbacks("👨‍👩‍👧‍👦12")).toBe("👨‍👩‍👧‍👦-12"); + expect(maskGraphWithFallbacks("1")).toBe("1"); + expect(maskGraphWithFallbacks("")).toBe(""); + }); + + test("creates a reusable masking function with codePoint and grapheme modes", (): void => { + const maskCodePoint: (value: string | null | undefined) => string = createMask("**-###", { mode: "codePoint" }); + const maskGrapheme: (value: string | null | undefined) => string = createMask("*-###", { mode: "grapheme" }); + + expect(maskCodePoint("😀😀123")).toBe("😀😀-123"); + expect(maskCodePoint("😀")).toBe("😀"); + expect(maskGrapheme("👨‍👩‍👧‍👦123")).toBe("👨‍👩‍👧‍👦-123"); + expect(maskGrapheme("👨‍👩‍👧‍👦")).toBe("👨‍👩‍👧‍👦"); + expect(maskCodePoint("")).toBe(""); + expect(maskGrapheme("")).toBe(""); + }); + }); + + describe("empty and unusual patterns", (): void => { + test("returns original value when pattern is empty and no fallbacks", (): void => { + expect(mask("Hello World", "")).toBe("Hello World"); + expect(mask("Hello World", "", { mode: "utf16" })).toBe("Hello World"); + expect(mask("Hello World", "", { mode: "codePoint" })).toBe("Hello World"); + expect(mask("Hello World", "", { mode: "grapheme" })).toBe("Hello World"); + expect(mask("Hello World", "", { fallbackPatterns: ["", ""] })).toBe("Hello World"); + expect(mask("Hello World", "", { fallbackPatterns: ["", ""], mode: "codePoint" })).toBe("Hello World"); + expect(mask("Hello World", "", { fallbackPatterns: ["", ""], mode: "grapheme" })).toBe("Hello World"); + + expect(createMask("")("Hello World")).toBe("Hello World"); + expect(createMask("", { mode: "codePoint" })("Hello World")).toBe("Hello World"); + expect(createMask("", { mode: "grapheme" })("Hello World")).toBe("Hello World"); + expect(createMask("", { fallbackPatterns: ["", ""] })("Hello World")).toBe("Hello World"); + expect(createMask("", { fallbackPatterns: ["", ""], mode: "codePoint" })("Hello World")).toBe("Hello World"); + expect(createMask("", { fallbackPatterns: ["", ""], mode: "grapheme" })("Hello World")).toBe("Hello World"); + expect(createMask("")("")).toBe(""); + }); + + test("handles literal-only patterns matching and non-matching", (): void => { + expect(mask("---", "---")).toBe("---"); + expect(mask("abc", "---")).toBe("abc"); + expect(mask("---", "---", { mode: "grapheme" })).toBe("---"); + expect(mask("abc", "---", { mode: "grapheme" })).toBe("abc"); + expect(mask("---", "--", { mode: "grapheme" })).toBe("---"); + expect(mask("---", "-x-", { mode: "grapheme" })).toBe("---"); + expect(mask("---", "---", { mode: "codePoint" })).toBe("---"); + expect(mask("abc", "---", { mode: "codePoint" })).toBe("abc"); + expect(mask("---", "--", { mode: "codePoint" })).toBe("---"); + expect(mask("---", "-x-", { mode: "codePoint" })).toBe("---"); + }); + }); + + describe("empty and nullish", (): void => { + test("returns an empty string", (): void => { + expect(mask("", "(###) ###-####")).toBe(""); + expect(mask("", "(###) ###-####", { fallbackPatterns: ["(##) ###-####"] })).toBe(""); + expect(mask("", "(###) ###-####", { mode: "codePoint" })).toBe(""); + expect(mask("", "(###) ###-####", { mode: "grapheme" })).toBe(""); + expect(mask(null, "(###) ###-####")).toBe(""); + expect(mask(undefined, "(###) ###-####")).toBe(""); + expect(mask(null, "(###) ###-####", { fallbackPatterns: ["(##) ###-####"] })).toBe(""); + expect(mask(undefined, "(###) ###-####", { fallbackPatterns: ["(##) ###-####"] })).toBe(""); + expect(mask(null, "(###) ###-####", { mode: "codePoint" })).toBe(""); + expect(mask(undefined, "(###) ###-####", { mode: "codePoint" })).toBe(""); + expect(mask(null, "(###) ###-####", { mode: "grapheme" })).toBe(""); + expect(mask(undefined, "(###) ###-####", { mode: "grapheme" })).toBe(""); + + const compiled: (value: string | null | undefined) => string = createMask("(###) ###-####"); + const compiledCP: (value: string | null | undefined) => string = createMask("**-###", { mode: "codePoint" }); + const compiledGraph: (value: string | null | undefined) => string = createMask("*-###", { mode: "grapheme" }); + const compiledEmpty: (value: string | null | undefined) => string = createMask(""); + + const compiledEmptyFallbacks: (value: string | null | undefined) => string = createMask("", { + fallbackPatterns: ["", ""] + }); + + expect(compiled(null)).toBe(""); + expect(compiled(undefined)).toBe(""); + expect(compiledCP(null)).toBe(""); + expect(compiledCP(undefined)).toBe(""); + expect(compiledGraph(null)).toBe(""); + expect(compiledGraph(undefined)).toBe(""); + expect(compiledEmpty(null)).toBe(""); + expect(compiledEmpty(undefined)).toBe(""); + expect(compiledEmptyFallbacks(null)).toBe(""); + expect(compiledEmptyFallbacks(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/mask/mask.lib.ts b/src/libs/mask/mask.lib.ts new file mode 100644 index 0000000..7158a44 --- /dev/null +++ b/src/libs/mask/mask.lib.ts @@ -0,0 +1,771 @@ +import { ASTERISK, EMPTY, HASH, UNICODE_DIGIT_REGEX, UNICODE_LETTER_REGEX } from "../../constants"; +import { CharacterSegmentMode } from "../../types"; + +import { hasLength } from "../has-length"; + +/** + * A reusable masking function returned by {@link createMask}. + * + * @param value - The string to mask, or `null` or `undefined`. + * @returns The formatted string, or an empty string when `value` is empty, `null`, or `undefined`. + */ +export type MaskFunction = (value: string | null | undefined) => string; + +/** + * Configuration options for {@link mask} and {@link createMask}. + */ +export interface MaskOptions { + /** + * Additional mask patterns to evaluate when the primary pattern does not match. + * + * Patterns are evaluated in declaration order. + * + * @default [] + */ + fallbackPatterns?: string[]; + + /** + * Determines how characters are counted and segmented. + * + * @default "utf16" + */ + mode?: CharacterSegmentMode; +} + +interface CompiledPatternUtf16 { + readonly pattern: string; + readonly totalSlots: number; +} + +interface CompiledPatternSegments { + readonly patternSegments: readonly string[]; + readonly totalSlots: number; +} + +const LETTER_A: string = "A"; +const CODE_HASH: number = 35; +const CODE_ASTERISK: number = 42; +const CODE_LETTER_A: number = 65; +const CODE_ZERO: number = 48; +const CODE_NINE: number = 57; +const CODE_UPPER_A: number = 65; +const CODE_UPPER_Z: number = 90; +const CODE_LOWER_A: number = 97; +const CODE_LOWER_Z: number = 122; +const ASCII_MAX: number = 127; + +let graphemeSegmenter: Intl.Segmenter | undefined; + +/** + * Lazily creates the grapheme segmenter used by `"grapheme"` mode. + */ +function getGraphemeSegmenter(): Intl.Segmenter { + if (!graphemeSegmenter) { + graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + } + + return graphemeSegmenter; +} + +/** + * Determines whether a character is a supported mask slot token. + * + * Supported tokens are `#`, `A`, and `*`. + */ +function isSlotToken(token: string): boolean { + return token === HASH || token === LETTER_A || token === ASTERISK; +} + +/** + * Determines whether a character represents a decimal digit. + * + * Supports ASCII digits (`0-9`) and Unicode decimal digits (`\p{Nd}`). + */ +function isDigit(char: string): boolean { + const charCode: number = char.charCodeAt(0); + + if (charCode >= CODE_ZERO && charCode <= CODE_NINE) { + return true; + } + + return charCode > ASCII_MAX && UNICODE_DIGIT_REGEX.test(char); +} + +/** + * Determines whether a character represents a letter or combining mark. + * + * Supports ASCII letters (`a-z`, `A-Z`) and Unicode letters or marks (`[\p{L}\p{M}]`). + */ +function isLetter(char: string): boolean { + const charCode: number = char.charCodeAt(0); + + if ( + (charCode >= CODE_UPPER_A && charCode <= CODE_UPPER_Z) || + (charCode >= CODE_LOWER_A && charCode <= CODE_LOWER_Z) + ) { + return true; + } + + return charCode > ASCII_MAX && UNICODE_LETTER_REGEX.test(char); +} + +/** + * Determines whether the character at the specified UTF-16 index is a decimal digit. + */ +function isDigitAt(value: string, charIndex: number): boolean { + const charCode: number = value.charCodeAt(charIndex); + + if (charCode >= CODE_ZERO && charCode <= CODE_NINE) { + return true; + } + + return charCode > ASCII_MAX && UNICODE_DIGIT_REGEX.test(value[charIndex]); +} + +/** + * Determines whether the character at the specified UTF-16 index is a letter or combining mark. + */ +function isLetterAt(value: string, charIndex: number): boolean { + const charCode: number = value.charCodeAt(charIndex); + + if ( + (charCode >= CODE_UPPER_A && charCode <= CODE_UPPER_Z) || + (charCode >= CODE_LOWER_A && charCode <= CODE_LOWER_Z) + ) { + return true; + } + + return charCode > ASCII_MAX && UNICODE_LETTER_REGEX.test(value[charIndex]); +} + +/** + * Advances to the next decimal digit in a UTF-16 string. + */ +function advanceToDigit(value: string, startIndex: number, length: number): number { + let currentIndex: number = startIndex; + + while (currentIndex < length && !isDigitAt(value, currentIndex)) { + currentIndex++; + } + + return currentIndex; +} + +/** + * Advances to the next letter in a UTF-16 string. + */ +function advanceToLetter(value: string, startIndex: number, length: number): number { + let currentIndex: number = startIndex; + + while (currentIndex < length && !isLetterAt(value, currentIndex)) { + currentIndex++; + } + + return currentIndex; +} + +/** + * Advances to the next value index matching a mask token in UTF-16 mode. + */ +function advanceSlotIndex(patternCharCode: number, value: string, startIndex: number, length: number): number { + if (patternCharCode === CODE_HASH) { + return advanceToDigit(value, startIndex, length); + } + + if (patternCharCode === CODE_LETTER_A) { + return advanceToLetter(value, startIndex, length); + } + + return startIndex; +} + +/** + * Advances to the next value segment matching a mask token in segmented mode. + */ +function advanceSlotSegmentIndex( + patternToken: string, + valueSegments: readonly string[], + startIndex: number, + length: number +): number { + if (patternToken === HASH) { + let currentIndex: number = startIndex; + + while (currentIndex < length && !isDigit(valueSegments[currentIndex])) { + currentIndex++; + } + + return currentIndex; + } + + if (patternToken === LETTER_A) { + let currentIndex: number = startIndex; + + while (currentIndex < length && !isLetter(valueSegments[currentIndex])) { + currentIndex++; + } + + return currentIndex; + } + + return startIndex; +} + +/** + * Verifies that all literal segments match between the pattern and value. + */ +function matchLiteralSegments( + patternSegments: readonly string[], + valueSegments: readonly string[], + length: number +): boolean { + for (let segmentIndex: number = 0; segmentIndex < length; segmentIndex++) { + if (patternSegments[segmentIndex] !== valueSegments[segmentIndex]) { + return false; + } + } + + return true; +} + +/** + * Handles matching for patterns that contain no mask slots. + */ +function tryMaskEmptySlotSegments( + patternSegments: readonly string[], + patternLength: number, + valueSegments: readonly string[], + valueLength: number +): string | null { + if (patternLength !== valueLength || !matchLiteralSegments(patternSegments, valueSegments, patternLength)) { + return null; + } + + return valueSegments.join(EMPTY); +} + +/** + * Counts mask slot tokens (`#`, `A`, and `*`) in a UTF-16 pattern. + */ +function countSlotsInString(pattern: string): number { + let totalSlots: number = 0; + + for (let charIndex: number = 0; charIndex < pattern.length; charIndex++) { + const charCode: number = pattern.charCodeAt(charIndex); + + if (charCode === CODE_HASH || charCode === CODE_LETTER_A || charCode === CODE_ASTERISK) { + totalSlots++; + } + } + + return totalSlots; +} + +/** + * Counts mask slot tokens (`#`, `A`, and `*`) in a segmented pattern. + */ +function countSlotsInSegments(segments: readonly string[]): number { + let totalSlots: number = 0; + + for (let segmentIndex: number = 0; segmentIndex < segments.length; segmentIndex++) { + if (isSlotToken(segments[segmentIndex])) { + totalSlots++; + } + } + + return totalSlots; +} + +/** + * Splits a string into user-perceived grapheme clusters. + */ +function splitGraphemes(value: string): string[] { + const segments: string[] = []; + + for (const segmentData of getGraphemeSegmenter().segment(value)) { + segments.push(segmentData.segment); + } + + return segments; +} + +/** + * Splits a string into Unicode code points. + */ +function splitCodePoints(value: string): string[] { + return Array.from(value); +} + +/** + * Compiles a UTF-16 mask pattern with pre-computed slot metadata. + */ +function compilePatternUtf16(pattern: string): CompiledPatternUtf16 { + return { + pattern, + totalSlots: countSlotsInString(pattern) + }; +} + +/** + * Compiles a segmented mask pattern with pre-computed slot metadata. + */ +function compilePatternSegments(patternSegments: string[]): CompiledPatternSegments { + return { + patternSegments, + totalSlots: countSlotsInSegments(patternSegments) + }; +} + +/** + * Checks whether a literal pattern code unit matches the current UTF-16 value code unit. + */ +function isCharLiteralMatched( + value: string, + valueIndex: number, + valueLength: number, + patternCharCode: number +): boolean { + return valueIndex < valueLength && value.charCodeAt(valueIndex) === patternCharCode; +} + +/** + * Checks whether a literal pattern segment matches the current value segment. + */ +function isSegmentLiteralMatched( + valueSegments: readonly string[], + valueIndex: number, + valueLength: number, + patternToken: string +): boolean { + return valueIndex < valueLength && valueSegments[valueIndex] === patternToken; +} + +/** + * Attempts to apply a UTF-16 mask pattern directly to a string. + * + * `#` matches a decimal digit, `A` matches a letter or combining mark, and `*` matches any single UTF-16 code unit. Other characters are treated as literals. + */ +function tryMaskUtf16Raw(pattern: string, totalSlots: number, value: string): string | null { + if (totalSlots === 0) { + return pattern === value ? pattern : null; + } + + const valueLength: number = value.length; + + if (valueLength < totalSlots) { + return null; + } + + const patternLength: number = pattern.length; + let result: string = EMPTY; + let valueIndex: number = 0; + let slotsFilled: number = 0; + + for (let patternIndex: number = 0; patternIndex < patternLength; patternIndex++) { + const patternCharCode: number = pattern.charCodeAt(patternIndex); + + if (patternCharCode === CODE_HASH || patternCharCode === CODE_LETTER_A || patternCharCode === CODE_ASTERISK) { + valueIndex = advanceSlotIndex(patternCharCode, value, valueIndex, valueLength); + + if (valueIndex >= valueLength) { + return null; + } + + result += value[valueIndex]; + valueIndex++; + slotsFilled++; + } else { + result += pattern[patternIndex]; + + if (isCharLiteralMatched(value, valueIndex, valueLength, patternCharCode)) { + valueIndex++; + } + } + } + + return slotsFilled !== totalSlots || valueIndex < valueLength ? null : result; +} + +/** + * Attempts to apply a pre-compiled UTF-16 mask pattern to a string. + */ +function tryMaskUtf16(compiled: CompiledPatternUtf16, value: string): string | null { + return tryMaskUtf16Raw(compiled.pattern, compiled.totalSlots, value); +} + +/** + * Attempts to apply a segmented mask pattern directly to segmented input. + * + * `#` matches a decimal digit, `A` matches a letter or combining mark, and `*` matches any single segment. Other segments are treated as literals. + */ +function tryMaskSegmentsRaw( + patternSegments: readonly string[], + totalSlots: number, + valueSegments: readonly string[] +): string | null { + const patternLength: number = patternSegments.length; + const valueLength: number = valueSegments.length; + + if (totalSlots === 0) { + return tryMaskEmptySlotSegments(patternSegments, patternLength, valueSegments, valueLength); + } + + if (valueLength < totalSlots) { + return null; + } + + let result: string = EMPTY; + let valueIndex: number = 0; + let slotsFilled: number = 0; + + for (let patternIndex: number = 0; patternIndex < patternLength; patternIndex++) { + const patternToken: string = patternSegments[patternIndex]; + + if (isSlotToken(patternToken)) { + valueIndex = advanceSlotSegmentIndex(patternToken, valueSegments, valueIndex, valueLength); + + if (valueIndex >= valueLength) { + return null; + } + + result += valueSegments[valueIndex]; + valueIndex++; + slotsFilled++; + } else { + result += patternToken; + + if (isSegmentLiteralMatched(valueSegments, valueIndex, valueLength, patternToken)) { + valueIndex++; + } + } + } + + if (slotsFilled !== totalSlots || valueIndex < valueLength) { + return null; + } + + return result; +} + +/** + * Attempts to apply a pre-compiled segmented mask pattern to segmented input. + */ +function tryMaskSegments(compiled: CompiledPatternSegments, valueSegments: readonly string[]): string | null { + return tryMaskSegmentsRaw(compiled.patternSegments, compiled.totalSlots, valueSegments); +} + +/** + * Executes pre-compiled UTF-16 patterns in declaration order. + */ +function executeMaskUtf16(compiledPatterns: readonly CompiledPatternUtf16[], value: string): string { + const totalPatterns: number = compiledPatterns.length; + + for (let patternIndex: number = 0; patternIndex < totalPatterns; patternIndex++) { + const formatted: string | null = tryMaskUtf16(compiledPatterns[patternIndex], value); + + if (formatted !== null) { + return formatted; + } + } + + return value; +} + +/** + * Executes pre-compiled segmented patterns in declaration order. + */ +function executeMaskSegments( + compiledPatterns: readonly CompiledPatternSegments[], + valueSegments: readonly string[], + value: string +): string { + const totalPatterns: number = compiledPatterns.length; + + for (let patternIndex: number = 0; patternIndex < totalPatterns; patternIndex++) { + const formatted: string | null = tryMaskSegments(compiledPatterns[patternIndex], valueSegments); + + if (formatted !== null) { + return formatted; + } + } + + return value; +} + +/** + * Collects non-empty primary and fallback mask patterns in declaration order. + */ +function collectValidPatterns(pattern: string, fallbackPatterns: readonly string[]): string[] { + const validPatterns: string[] = []; + + if (pattern.length > 0) { + validPatterns.push(pattern); + } + + const fallbackCount: number = fallbackPatterns.length; + + for (let fallbackIndex: number = 0; fallbackIndex < fallbackCount; fallbackIndex++) { + const fallbackItem: string = fallbackPatterns[fallbackIndex]; + + if (fallbackItem.length > 0) { + validPatterns.push(fallbackItem); + } + } + + return validPatterns; +} + +/** + * Formats a string according to a mask pattern. + * + * Supported slot tokens: + * - `#`: Decimal digit (`0-9` or `\p{Nd}`). + * - `A`: Letter or combining mark (`a-z`, `A-Z`, or `[\p{L}\p{M}]`). + * - `*`: Wildcard character. + * + * Returns the original string when `value` does not match the pattern. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param value - The string to mask, or `null` or `undefined`. + * @param pattern - The mask template pattern. + * @returns The formatted string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * mask("0812345678", "(###) ###-####"); // "(081) 234-5678" + * mask("1234567890123456", "#### #### #### ####"); // "1234 5678 9012 3456" + * mask("ABC1234", "AAA-####"); // "ABC-1234" + * mask("K9V1B2", "A#A #A#"); // "K9V 1B2" + * mask("12345", "(###) ###-####"); // "12345" + * mask("", "(###) ###-####"); // "" + * mask(null, "(###) ###-####"); // "" + * mask(undefined, "(###) ###-####"); // "" + */ +export function mask(value: string | null | undefined, pattern: string): string; + +/** + * Formats a string according to a mask pattern using custom options. + * + * The `mode` option determines how characters are counted. + * + * Returns the original string when `value` does not match the pattern or any fallback pattern. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param value - The string to mask, or `null` or `undefined`. + * @param pattern - The primary mask template pattern. + * @param options - Masking options. + * @returns The formatted string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * mask("021234567", "(###) ###-####", { fallbackPatterns: ["(##) ###-####"] }); // "(02) 123-4567" + * mask("0812345678", "(###) ###-####", { fallbackPatterns: ["(##) ###-####"] }); // "(081) 234-5678" + * mask("12345", "(###) ###-####", { fallbackPatterns: ["(##) ###-####"] }); // "12345" + * mask("😀😀123", "**-###", { mode: "codePoint" }); // "😀😀-123" + * mask("", "(###) ###-####"); // "" + * mask(null, "(###) ###-####"); // "" + * mask(undefined, "(###) ###-####"); // "" + */ +export function mask(value: string | null | undefined, pattern: string, options: MaskOptions): string; + +export function mask(value: string | null | undefined, pattern: string, options?: MaskOptions): string { + if (!hasLength(value)) { + return EMPTY; + } + + if (!options) { + if (pattern.length === 0) { + return value; + } + + const totalSlots: number = countSlotsInString(pattern); + const masked: string | null = tryMaskUtf16Raw(pattern, totalSlots, value); + + return masked ?? value; + } + + const { fallbackPatterns, mode = "utf16" }: MaskOptions = options; + + if (!fallbackPatterns || fallbackPatterns.length === 0) { + if (pattern.length === 0) { + return value; + } + + if (mode === "utf16") { + const totalSlots: number = countSlotsInString(pattern); + const masked: string | null = tryMaskUtf16Raw(pattern, totalSlots, value); + + return masked ?? value; + } + + if (mode === "codePoint") { + const patternSegments: string[] = splitCodePoints(pattern); + const totalSlots: number = countSlotsInSegments(patternSegments); + const valueSegments: string[] = splitCodePoints(value); + const masked: string | null = tryMaskSegmentsRaw(patternSegments, totalSlots, valueSegments); + + return masked ?? value; + } + + const patternSegments: string[] = splitGraphemes(pattern); + const totalSlots: number = countSlotsInSegments(patternSegments); + const valueSegments: string[] = splitGraphemes(value); + const masked: string | null = tryMaskSegmentsRaw(patternSegments, totalSlots, valueSegments); + + return masked ?? value; + } + + const allPatterns: string[] = collectValidPatterns(pattern, fallbackPatterns); + + if (allPatterns.length === 0) { + return value; + } + + if (mode === "grapheme") { + const compiledGraphemes: CompiledPatternSegments[] = allPatterns.map(splitGraphemes).map(compilePatternSegments); + + return executeMaskSegments(compiledGraphemes, splitGraphemes(value), value); + } + + if (mode === "codePoint") { + const compiledCodePoints: CompiledPatternSegments[] = allPatterns.map(splitCodePoints).map(compilePatternSegments); + + return executeMaskSegments(compiledCodePoints, splitCodePoints(value), value); + } + + const compiledUtf16: CompiledPatternUtf16[] = allPatterns.map(compilePatternUtf16); + + return executeMaskUtf16(compiledUtf16, value); +} + +/** + * Creates a reusable masking function for a mask pattern. + * + * Returns the original string when `value` does not match the pattern. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param pattern - The mask template pattern. + * @returns A function that masks input strings according to the pattern. + * + * @example + * + * const maskPhone = createMask("(###) ###-####"); + * + * maskPhone("0812345678"); // "(081) 234-5678" + * maskPhone("12345"); // "12345" + * maskPhone(""); // "" + * maskPhone(null); // "" + * maskPhone(undefined); // "" + */ +export function createMask(pattern: string): MaskFunction; + +/** + * Creates a reusable masking function using custom options. + * + * The `mode` option determines how characters are counted. + * + * Returns the original string when `value` does not match the pattern or any fallback pattern. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param pattern - The primary mask template pattern. + * @param options - Masking options. + * @returns A function that masks input strings according to the pattern and options. + * + * @example + * + * const maskPhone = createMask("(###) ###-####", { fallbackPatterns: ["(##) ###-####"] }); + * + * maskPhone("021234567"); // "(02) 123-4567" + * maskPhone("0812345678"); // "(081) 234-5678" + * maskPhone("12345"); // "12345" + * maskPhone(""); // "" + * maskPhone(null); // "" + * maskPhone(undefined); // "" + */ +export function createMask(pattern: string, options: MaskOptions): MaskFunction; + +export function createMask(pattern: string, options?: MaskOptions): MaskFunction { + const { fallbackPatterns, mode = "utf16" }: MaskOptions = options ?? {}; + + if (!fallbackPatterns || fallbackPatterns.length === 0) { + if (pattern.length === 0) { + return (value: string | null | undefined): string => { + return value ?? EMPTY; + }; + } + + if (mode === "utf16") { + const totalSlots: number = countSlotsInString(pattern); + + return (value: string | null | undefined): string => { + if (!hasLength(value)) { + return EMPTY; + } + + const masked: string | null = tryMaskUtf16Raw(pattern, totalSlots, value); + + return masked ?? value; + }; + } + + if (mode === "codePoint") { + const patternSegments: string[] = splitCodePoints(pattern); + const totalSlots: number = countSlotsInSegments(patternSegments); + + return (value: string | null | undefined): string => { + if (!hasLength(value)) { + return EMPTY; + } + + const valueSegments: string[] = splitCodePoints(value); + const masked: string | null = tryMaskSegmentsRaw(patternSegments, totalSlots, valueSegments); + + return masked ?? value; + }; + } + + const patternSegments: string[] = splitGraphemes(pattern); + const totalSlots: number = countSlotsInSegments(patternSegments); + + return (value: string | null | undefined): string => { + if (!hasLength(value)) { + return EMPTY; + } + + const valueSegments: string[] = splitGraphemes(value); + const masked: string | null = tryMaskSegmentsRaw(patternSegments, totalSlots, valueSegments); + + return masked ?? value; + }; + } + + const allPatterns: string[] = collectValidPatterns(pattern, fallbackPatterns); + + if (allPatterns.length === 0) { + return (value: string | null | undefined): string => { + return value ?? EMPTY; + }; + } + + if (mode === "grapheme") { + const compiledGraphemes: CompiledPatternSegments[] = allPatterns.map(splitGraphemes).map(compilePatternSegments); + + return (value: string | null | undefined): string => { + return !hasLength(value) ? EMPTY : executeMaskSegments(compiledGraphemes, splitGraphemes(value), value); + }; + } + + if (mode === "codePoint") { + const compiledCodePoints: CompiledPatternSegments[] = allPatterns.map(splitCodePoints).map(compilePatternSegments); + + return (value: string | null | undefined): string => { + return !hasLength(value) ? EMPTY : executeMaskSegments(compiledCodePoints, splitCodePoints(value), value); + }; + } + + const compiledUtf16: CompiledPatternUtf16[] = allPatterns.map(compilePatternUtf16); + + return (value: string | null | undefined): string => { + return !hasLength(value) ? EMPTY : executeMaskUtf16(compiledUtf16, value); + }; +} diff --git a/src/libs/pascal-case/index.ts b/src/libs/pascal-case/index.ts new file mode 100644 index 0000000..9a53491 --- /dev/null +++ b/src/libs/pascal-case/index.ts @@ -0,0 +1 @@ +export * from "./pascal-case.lib"; diff --git a/src/libs/pascal-case/pascal-case.lib.bench.ts b/src/libs/pascal-case/pascal-case.lib.bench.ts new file mode 100644 index 0000000..59b2bf7 --- /dev/null +++ b/src/libs/pascal-case/pascal-case.lib.bench.ts @@ -0,0 +1,111 @@ +import { describe, test, TestContext } from "vitest"; + +import { pascalCase } from "./pascal-case.lib"; + +const shortPascalCase: string = "XMLHttpRequestResponseCodeHandler"; +const input1KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(20); +const input10KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(200); +const input100KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(2000); +const longUnicodeInput: string = "Привет-Мир café-noir 你好-世界 こんにちは-世界 مرحبا-بالعالم ".repeat(100); +const separatorHeavyInput: string = "---foo---bar___baz...qux!!!".repeat(1000); +const mixedCaseInput: string = "aAaAaAaAaAaAaAaAaAaAaAaAaAaAaA".repeat(1000); + +describe("pascalCase", (): void => { + test("given common short ASCII inputs", ({ bench }: TestContext): void => { + bench("hyphen-separated lowercase", (): void => { + pascalCase("foo-bar"); + }); + + bench("underscore-separated uppercase", (): void => { + pascalCase("FOO_BAR"); + }); + + bench("camelCase", (): void => { + pascalCase("fooBar"); + }); + + bench("already PascalCase", (): void => { + pascalCase(shortPascalCase); + }); + + bench("acronym followed by word", (): void => { + pascalCase("HTTPResponseCode"); + }); + + bench("mixed words and numbers", (): void => { + pascalCase("version-2-0"); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + pascalCase(input1KB); + }); + + bench("approximately 10 KB input", (): void => { + pascalCase(input10KB); + }); + + bench("approximately 100 KB input", (): void => { + pascalCase(input100KB); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("long Unicode input", (): void => { + pascalCase(longUnicodeInput); + }); + + bench("combining marks", (): void => { + pascalCase("cafe\u0301 noir"); + }); + + bench("Cyrillic", (): void => { + pascalCase("Привет Мир"); + }); + + bench("CJK", (): void => { + pascalCase("你好 世界"); + }); + + bench("Arabic", (): void => { + pascalCase("مرحبا بالعالم"); + }); + }); + + test("given separator-heavy input", ({ bench }: TestContext): void => { + bench("long separator-heavy input", (): void => { + pascalCase(separatorHeavyInput); + }); + }); + + test("given mixed-case input", ({ bench }: TestContext): void => { + bench("long mixed-case input", (): void => { + pascalCase(mixedCaseInput); + }); + }); + + test("given simple inputs", ({ bench }: TestContext): void => { + bench("single word", (): void => { + pascalCase("hello"); + }); + + bench("numeric-only input", (): void => { + pascalCase("123456789"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + pascalCase(""); + }); + + bench("null", (): void => { + pascalCase(null); + }); + + bench("undefined", (): void => { + pascalCase(undefined); + }); + }); +}); diff --git a/src/libs/pascal-case/pascal-case.lib.spec.ts b/src/libs/pascal-case/pascal-case.lib.spec.ts new file mode 100644 index 0000000..37258e6 --- /dev/null +++ b/src/libs/pascal-case/pascal-case.lib.spec.ts @@ -0,0 +1,173 @@ +import { describe, expect, test } from "vitest"; + +import { pascalCase } from "./pascal-case.lib"; + +describe("pascalCase", (): void => { + describe("given a standard string with separators", (): void => { + test("returns the PascalCase string", (): void => { + expect(pascalCase("foo-bar")).toBe("FooBar"); + expect(pascalCase("foo_bar")).toBe("FooBar"); + expect(pascalCase("foo.bar")).toBe("FooBar"); + expect(pascalCase("foo bar")).toBe("FooBar"); + expect(pascalCase("--foo-bar--")).toBe("FooBar"); + expect(pascalCase("__foo_bar__")).toBe("FooBar"); + expect(pascalCase(" foo bar ")).toBe("FooBar"); + }); + + test("handles mixed separators", (): void => { + expect(pascalCase("foo-bar_baz.qux")).toBe("FooBarBazQux"); + expect(pascalCase("foo / bar \\ baz")).toBe("FooBarBaz"); + expect(pascalCase("foo!!!bar")).toBe("FooBar"); + expect(pascalCase("foo---bar")).toBe("FooBar"); + expect(pascalCase("foo...bar")).toBe("FooBar"); + }); + }); + + describe("given an uppercase string with separators", (): void => { + test("returns the PascalCase string", (): void => { + expect(pascalCase("FOO-BAR")).toBe("FooBar"); + expect(pascalCase("FOO_BAR")).toBe("FooBar"); + expect(pascalCase("FOO BAR")).toBe("FooBar"); + expect(pascalCase("FOO-BAR-BAZ")).toBe("FooBarBaz"); + }); + }); + + describe("given a camelCase string", (): void => { + test("returns the PascalCase string", (): void => { + expect(pascalCase("fooBar")).toBe("FooBar"); + expect(pascalCase("fooBarBaz")).toBe("FooBarBaz"); + expect(pascalCase("xmlHttpRequest")).toBe("XmlHttpRequest"); + }); + }); + + describe("given a PascalCase string", (): void => { + test("returns the original PascalCase string", (): void => { + expect(pascalCase("FooBar")).toBe("FooBar"); + expect(pascalCase("FooBarBaz")).toBe("FooBarBaz"); + expect(pascalCase("XmlHttpRequest")).toBe("XmlHttpRequest"); + }); + }); + + describe("given acronyms and mixed casing", (): void => { + test("handles consecutive uppercase letters", (): void => { + expect(pascalCase("HTTPResponseCode")).toBe("HttpResponseCode"); + expect(pascalCase("JSONDataAPI")).toBe("JsonDataApi"); + expect(pascalCase("XMLHttpRequest")).toBe("XmlHttpRequest"); + expect(pascalCase("parseHTTPResponse")).toBe("ParseHttpResponse"); + expect(pascalCase("getURLValue")).toBe("GetUrlValue"); + }); + }); + + describe("given strings containing numbers", (): void => { + test("handles numbers as separate words", (): void => { + expect(pascalCase("version 2.0")).toBe("Version20"); + expect(pascalCase("version-2-0")).toBe("Version20"); + expect(pascalCase("version_2_0")).toBe("Version20"); + expect(pascalCase("2foo")).toBe("2Foo"); + expect(pascalCase("foo2bar")).toBe("Foo2Bar"); + }); + + test("preserves numeric-only input", (): void => { + expect(pascalCase("123")).toBe("123"); + expect(pascalCase("123-456")).toBe("123456"); + expect(pascalCase("123_456_789")).toBe("123456789"); + }); + }); + + describe("given strings with accents and Unicode", (): void => { + test("handles accented letters as part of words", (): void => { + expect(pascalCase("crème-brûlée")).toBe("CrèmeBrûlée"); + expect(pascalCase("jalapeño popper")).toBe("JalapeñoPopper"); + expect(pascalCase("École Normale")).toBe("ÉcoleNormale"); + }); + + test("handles Cyrillic casing", (): void => { + expect(pascalCase("Привет Мир")).toBe("ПриветМир"); + expect(pascalCase("привет-мир")).toBe("ПриветМир"); + expect(pascalCase("ПРИВЕТ МИР")).toBe("ПриветМир"); + }); + + test("preserves combining marks", (): void => { + expect(pascalCase("cafe\u0301 noir")).toBe("Cafe\u0301Noir"); + expect(pascalCase("Cafe\u0301 Noir")).toBe("Cafe\u0301Noir"); + }); + }); + + describe("given non-cased scripts", (): void => { + test("joins CJK, Hiragana, and Hangul without case transformation", (): void => { + expect(pascalCase("こんにちは 世界")).toBe("こんにちは世界"); + expect(pascalCase("你好 世界")).toBe("你好世界"); + expect(pascalCase("안녕하세요 세계")).toBe("안녕하세요세계"); + }); + + test("joins Arabic words without case transformation", (): void => { + expect(pascalCase("مرحبا بالعالم")).toBe("مرحبابالعالم"); + }); + + test("handles mixed Latin and non-cased scripts", (): void => { + expect(pascalCase("hello 世界")).toBe("Hello世界"); + expect(pascalCase("Hello 世界")).toBe("Hello世界"); + expect(pascalCase("foo-こんにちは")).toBe("Fooこんにちは"); + expect(pascalCase("hello 世界 Привет")).toBe("Hello世界Привет"); + }); + }); + + describe("given strings containing only symbols or separators", (): void => { + test("returns an empty string when no words are extractable", (): void => { + expect(pascalCase("---")).toBe(""); + expect(pascalCase("___")).toBe(""); + expect(pascalCase("...")).toBe(""); + expect(pascalCase("!!!")).toBe(""); + expect(pascalCase("@#$%^&*")).toBe(""); + }); + }); + + describe("given a single word", (): void => { + test("returns the capitalized word", (): void => { + expect(pascalCase("hello")).toBe("Hello"); + expect(pascalCase("Hello")).toBe("Hello"); + expect(pascalCase("HELLO")).toBe("Hello"); + }); + + test("preserves a numeric-only word", (): void => { + expect(pascalCase("123456789")).toBe("123456789"); + }); + }); + + describe("given a PascalCase result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "foo-bar", + "FOO_BAR", + "FooBar", + "HTTPResponseCode", + "JSONDataAPI", + "version-2-0", + "crème-brûlée", + "Привет Мир", + "hello 世界" + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = pascalCase(input); + + expect(pascalCase(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(pascalCase("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(pascalCase(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(pascalCase(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/pascal-case/pascal-case.lib.ts b/src/libs/pascal-case/pascal-case.lib.ts new file mode 100644 index 0000000..29b0e51 --- /dev/null +++ b/src/libs/pascal-case/pascal-case.lib.ts @@ -0,0 +1,52 @@ +import { EMPTY, WORD_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Converts a string to PascalCase. + * + * Handles separators, existing casing styles, acronyms, numbers, Unicode characters, and combining marks. + * + * @param value - The string to convert to PascalCase, or `null` or `undefined`. + * @returns The PascalCase string, or an empty string when `value` is empty, `null`, or `undefined`, or contains no extractable words. + * + * @example + * + * pascalCase("Foo Bar"); // "FooBar" + * pascalCase("--foo-bar--"); // "FooBar" + * pascalCase("FOO_BAR"); // "FooBar" + * pascalCase("fooBar"); // "FooBar" + * pascalCase("XMLHttpRequest"); // "XmlHttpRequest" + * pascalCase("version 2.0"); // "Version20" + * pascalCase("Привет Мир"); // "ПриветМир" + * pascalCase("你好 世界"); // "你好世界" + * pascalCase("cafe\u0301 noir"); // "Cafe\u0301Noir" + * pascalCase("---"); // "" + * pascalCase(""); // "" + * pascalCase(null); // "" + * pascalCase(undefined); // "" + */ +export function pascalCase(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const words: RegExpMatchArray | null = value.match(WORD_REGEX); + + if (!words) { + return EMPTY; + } + + const length: number = words.length; + const firstWord: string = words[0]; + + let result: string = firstWord[0].toUpperCase() + firstWord.slice(1).toLowerCase(); + + for (let index: number = 1; index < length; index++) { + const word: string = words[index]; + + result += word[0].toUpperCase() + word.slice(1).toLowerCase(); + } + + return result; +} diff --git a/src/libs/redact/index.ts b/src/libs/redact/index.ts new file mode 100644 index 0000000..08dce8b --- /dev/null +++ b/src/libs/redact/index.ts @@ -0,0 +1 @@ +export * from "./redact.lib"; diff --git a/src/libs/redact/redact.lib.bench.ts b/src/libs/redact/redact.lib.bench.ts new file mode 100644 index 0000000..1c60832 --- /dev/null +++ b/src/libs/redact/redact.lib.bench.ts @@ -0,0 +1,145 @@ +import { describe, test, TestContext } from "vitest"; + +import { redact } from "./redact.lib"; + +const longText: string = "the quick brown fox jumps over the lazy dog 0812345678 ".repeat(100); +const longUnicode: string = "😀👨‍👩‍👧‍👦cafe\u0301 你好мир ".repeat(100); +const longSeparators: string = "---foo---bar---baz---".repeat(100); + +describe("redact", (): void => { + test("short ASCII", ({ bench }: TestContext): void => { + bench("default options", (): void => { + redact("0812345678"); + }); + + bench("custom replacement string", (): void => { + redact("0812345678", "#"); + }); + + bench("visibleStart number shorthand", (): void => { + redact("0812345678", 4); + }); + + bench("options object visibleStart and visibleEnd", (): void => { + redact("0812345678", { visibleStart: 3, visibleEnd: 2 }); + }); + }); + + test("long input", ({ bench }: TestContext): void => { + bench("long ASCII default", (): void => { + redact(longText); + }); + + bench("long ASCII with visibleStart and visibleEnd", (): void => { + redact(longText, { visibleStart: 10, visibleEnd: 10 }); + }); + + bench("long ASCII with custom replacement", (): void => { + redact(longText, { visibleStart: 10, visibleEnd: 10, replacement: "X" }); + }); + + bench("long Unicode [codePoint]", (): void => { + redact(longUnicode, { visibleStart: 5, visibleEnd: 5, mode: "codePoint" }); + }); + + bench("long Unicode [grapheme]", (): void => { + redact(longUnicode, { visibleStart: 5, visibleEnd: 5, mode: "grapheme" }); + }); + }); + + test("Unicode and combining marks", ({ bench }: TestContext): void => { + bench("accented Latin with combining marks (NFD)", (): void => { + redact("cafe\u0301 noir", { visibleStart: 4 }); + }); + + bench("CJK and Arabic words", (): void => { + redact("你好 世界 مرحبا بالعالم", { visibleStart: 2, visibleEnd: 2 }); + }); + + bench("emoji [codePoint]", (): void => { + redact("😀😀cafe\u0301 noir", { visibleStart: 1, visibleEnd: 1, mode: "codePoint" }); + }); + + bench("emoji [grapheme]", (): void => { + redact("😀👨‍👩‍👧‍👦cafe\u0301 noir", { visibleStart: 1, visibleEnd: 1, mode: "grapheme" }); + }); + }); + + test("excessive separators", ({ bench }: TestContext): void => { + bench("short separators with visibility", (): void => { + redact("---foo---bar---baz---1234567890---", { + visibleStart: 5, + visibleEnd: 5, + replacement: "*" + }); + }); + + bench("long separators with visibility", (): void => { + redact(longSeparators, { visibleStart: 10, visibleEnd: 10 }); + }); + }); + + test("3-mode comparison", ({ bench }: TestContext): void => { + bench("ASCII [utf16]", (): void => { + redact("Hello World", { visibleStart: 2, visibleEnd: 2, mode: "utf16" }); + }); + + bench("ASCII [codePoint]", (): void => { + redact("Hello World", { visibleStart: 2, visibleEnd: 2, mode: "codePoint" }); + }); + + bench("ASCII [grapheme]", (): void => { + redact("Hello World", { visibleStart: 2, visibleEnd: 2, mode: "grapheme" }); + }); + + bench("Emoji [utf16]", (): void => { + redact("😀😀😀😀", { visibleStart: 1, visibleEnd: 1, mode: "utf16" }); + }); + + bench("Emoji [codePoint]", (): void => { + redact("😀😀😀😀", { visibleStart: 1, visibleEnd: 1, mode: "codePoint" }); + }); + + bench("Emoji [grapheme]", (): void => { + redact("😀😀😀😀", { visibleStart: 1, visibleEnd: 1, mode: "grapheme" }); + }); + + bench("NFD [utf16]", (): void => { + redact("cafe\u0301 noir", { visibleStart: 4, visibleEnd: 2, mode: "utf16" }); + }); + + bench("NFD [codePoint]", (): void => { + redact("cafe\u0301 noir", { visibleStart: 4, visibleEnd: 2, mode: "codePoint" }); + }); + + bench("NFD [grapheme]", (): void => { + redact("cafe\u0301 noir", { visibleStart: 4, visibleEnd: 2, mode: "grapheme" }); + }); + + bench("ZWJ emoji [utf16]", (): void => { + redact("👨‍👩‍👧‍👦 👩‍💻", { visibleStart: 1, mode: "utf16" }); + }); + + bench("ZWJ emoji [codePoint]", (): void => { + redact("👨‍👩‍👧‍👦 👩‍💻", { visibleStart: 1, mode: "codePoint" }); + }); + + bench("ZWJ emoji [grapheme]", (): void => { + redact("👨‍👩‍👧‍👦 👩‍💻", { visibleStart: 1, mode: "grapheme" }); + }); + }); + + test("empty and nullish", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + redact(""); + }); + + bench("null", (): void => { + redact(null); + }); + + bench("undefined", (): void => { + redact(undefined); + }); + }); +}); diff --git a/src/libs/redact/redact.lib.spec.ts b/src/libs/redact/redact.lib.spec.ts new file mode 100644 index 0000000..092e791 --- /dev/null +++ b/src/libs/redact/redact.lib.spec.ts @@ -0,0 +1,128 @@ +import { describe, expect, test } from "vitest"; + +import { redact } from "./redact.lib"; + +describe("redact", (): void => { + describe("short ASCII", (): void => { + test("redacts the entire string with asterisks by default", (): void => { + expect(redact("0812345678")).toBe("**********"); + expect(redact("Hello World")).toBe("***********"); + }); + + test("redacts using a custom replacement string", (): void => { + expect(redact("0812345678", "#")).toBe("##########"); + expect(redact("12345", "XX")).toBe("XXXXXXXXXX"); + }); + + test("preserves the specified number of starting characters", (): void => { + expect(redact("0812345678", 2)).toBe("08********"); + expect(redact("0812345678", 4)).toBe("0812******"); + expect(redact("0812345678", 10)).toBe("0812345678"); + expect(redact("0812345678", 12)).toBe("0812345678"); + expect(redact("0812345678", -1)).toBe("**********"); + expect(redact("0812345678", 0)).toBe("**********"); + expect(redact("0812345678", Number.NaN)).toBe("**********"); + expect(redact("0812345678", Number.POSITIVE_INFINITY)).toBe("**********"); + expect(redact("0812345678", 2.9)).toBe("08********"); + }); + }); + + describe("options object", (): void => { + test("handles visibleStart and visibleEnd", (): void => { + expect(redact("0812345678", { visibleStart: 3, visibleEnd: 2 })).toBe("081*****78"); + expect(redact("0812345678", { visibleStart: 2, replacement: "#" })).toBe("08########"); + expect(redact("0812345678", { visibleEnd: 2, replacement: "#" })).toBe("########78"); + expect(redact("0812345678", { visibleEnd: 2 })).toBe("********78"); + expect(redact("0812345678", { visibleStart: 2 })).toBe("08********"); + expect(redact("0812345678", { replacement: "#" })).toBe("##########"); + expect(redact("0812345678", {})).toBe("**********"); + expect(redact("0812345678", { visibleStart: 3, visibleEnd: 2, replacement: "#" })).toBe("081#####78"); + expect(redact("1234567890", { visibleStart: 4, visibleEnd: 4, replacement: "X" })).toBe("1234XX7890"); + expect(redact("12345", { visibleStart: 0, visibleEnd: 0 })).toBe("*****"); + expect(redact("12345", { visibleStart: -2, visibleEnd: -3 })).toBe("*****"); + expect(redact("12345", { visibleStart: Number.NaN, visibleEnd: Number.NaN })).toBe("*****"); + expect(redact("12345", { visibleStart: Number.POSITIVE_INFINITY, visibleEnd: 0 })).toBe("*****"); + expect(redact("12345", { visibleStart: 0, visibleEnd: Number.POSITIVE_INFINITY })).toBe("*****"); + expect(redact("12345", { visibleStart: 2.7, visibleEnd: 1.2 })).toBe("12**5"); + }); + + test("returns the original string when visibility exceeds length", (): void => { + expect(redact("123", { visibleStart: 2, visibleEnd: 2 })).toBe("123"); + expect(redact("123", { visibleStart: 3 })).toBe("123"); + expect(redact("123", { visibleEnd: 3 })).toBe("123"); + expect(redact("123", { visibleStart: 4 })).toBe("123"); + }); + + test("returns the original string when surrogate adjustments cover the whole string", (): void => { + expect(redact("😀a", { visibleStart: 1, visibleEnd: 1 })).toBe("😀a"); + expect(redact("a😀", { visibleStart: 1, visibleEnd: 1 })).toBe("a😀"); + expect(redact("😀😀", { visibleStart: 1, visibleEnd: 1 })).toBe("😀😀"); + }); + }); + + describe("Unicode and combining marks", (): void => { + test("redacts Unicode characters using default UTF-16 mode", (): void => { + expect(redact("Привет")).toBe("******"); + expect(redact("你好世界")).toBe("****"); + expect(redact("مرحبا")).toBe("*****"); + expect(redact("สวัสดี")).toBe("******"); + expect(redact("café")).toBe("****"); + expect(redact("cafe\u0301")).toBe("*****"); + expect(redact(" \u00A0 \u3000 ")).toBe("*****"); + expect(redact("😀")).toBe("**"); + expect(redact("😀", { mode: "utf16" })).toBe("**"); + }); + + test("prevents surrogate pair splitting in default mode", (): void => { + expect(redact("😀1234", 1)).toBe("😀****"); + expect(redact("1234😀", { visibleEnd: 1 })).toBe("****😀"); + expect(redact("😀1234😀", { visibleStart: 1, visibleEnd: 1 })).toBe("😀****😀"); + expect(redact("\uD83D", { mode: "utf16" })).toBe("*"); + expect(redact("\uDC00", { mode: "utf16" })).toBe("*"); + expect(redact("\uD83D\uD83D", 1)).toBe("\uD83D*"); + expect(redact("\uD83D\uD83D", { visibleEnd: 1 })).toBe("*\uD83D"); + expect(redact("a\uDC00", { visibleEnd: 1 })).toBe("*\uDC00"); + }); + + test("redacts per Unicode code point when mode is 'codePoint'", (): void => { + expect(redact("😀", { mode: "codePoint" })).toBe("*"); + expect(redact("😀😀😀", { mode: "codePoint" })).toBe("***"); + expect(redact("😀😀😀", { mode: "codePoint", replacement: "#" })).toBe("###"); + expect(redact("cafe\u0301", { mode: "codePoint" })).toBe("*****"); + expect(redact("😀123😀", { visibleStart: 1, visibleEnd: 1, mode: "codePoint" })).toBe("😀***😀"); + expect(redact("😀123😀", { visibleStart: 1, mode: "codePoint" })).toBe("😀****"); + expect(redact("😀123😀", { visibleEnd: 1, mode: "codePoint" })).toBe("****😀"); + expect(redact("😀", { visibleStart: 1, mode: "codePoint" })).toBe("😀"); + expect(redact("😀😁", { visibleStart: 1, visibleEnd: 1, mode: "codePoint" })).toBe("😀😁"); + expect(redact("😀😁", { visibleStart: 3, mode: "codePoint" })).toBe("😀😁"); + expect(redact("\uD83D", { mode: "codePoint" })).toBe("*"); + expect(redact("\uD83Da", { mode: "codePoint" })).toBe("**"); + }); + + test("redacts per grapheme cluster when mode is 'grapheme'", (): void => { + expect(redact("😀", { mode: "grapheme" })).toBe("*"); + expect(redact("😀😀😀", { mode: "grapheme" })).toBe("***"); + expect(redact("😀😀😀", { mode: "grapheme", replacement: "#" })).toBe("###"); + expect(redact("👨‍👩‍👧‍👦", { mode: "grapheme" })).toBe("*"); + expect(redact("cafe\u0301", { mode: "grapheme" })).toBe("****"); + expect(redact("😀123😀", { visibleStart: 1, visibleEnd: 1, mode: "grapheme" })).toBe("😀***😀"); + expect(redact("😀123😀", { visibleStart: 1, mode: "grapheme" })).toBe("😀****"); + expect(redact("😀123😀", { visibleEnd: 1, mode: "grapheme" })).toBe("****😀"); + expect(redact("😀", { visibleStart: 1, mode: "grapheme" })).toBe("😀"); + expect(redact("👨‍👩‍👧‍👦👨‍👩‍👧‍👦", { visibleStart: 1, visibleEnd: 1, mode: "grapheme" })).toBe("👨‍👩‍👧‍👦👨‍👩‍👧‍👦"); + expect(redact("👨‍👩‍👧‍👦👨‍👩‍👧‍👦", { visibleStart: 3, mode: "grapheme" })).toBe("👨‍👩‍👧‍👦👨‍👩‍👧‍👦"); + }); + }); + + describe("empty and nullish", (): void => { + test("returns an empty string", (): void => { + expect(redact("")).toBe(""); + expect(redact(null)).toBe(""); + expect(redact(undefined)).toBe(""); + expect(redact("", "#")).toBe(""); + expect(redact(null, "#")).toBe(""); + expect(redact(undefined, 2)).toBe(""); + expect(redact(null, { visibleStart: 2 })).toBe(""); + }); + }); +}); diff --git a/src/libs/redact/redact.lib.ts b/src/libs/redact/redact.lib.ts new file mode 100644 index 0000000..4fb7067 --- /dev/null +++ b/src/libs/redact/redact.lib.ts @@ -0,0 +1,447 @@ +import { ASTERISK, EMPTY } from "../../constants"; +import { CharacterSegmentMode } from "../../types"; + +import { hasLength } from "../has-length"; + +/** + * Configuration options for {@link redact}. + */ +export interface RedactOptions { + /** + * Number of visible characters to preserve at the start of the string. + * + * Non-finite, negative, and zero values default to `0`. + * Fractional values are truncated toward zero. + * + * @default 0 + */ + visibleStart?: number; + + /** + * Number of visible characters to preserve at the end of the string. + * + * Non-finite, negative, and zero values default to `0`. + * Fractional values are truncated toward zero. + * + * @default 0 + */ + visibleEnd?: number; + + /** + * String used to replace each redacted character. + * + * @default "*" + */ + replacement?: string; + + /** + * Determines how characters are counted and segmented. + * + * @default "utf16" + */ + mode?: CharacterSegmentMode; +} + +const SURROGATE_LEAD_MIN: number = 0xd800; +const SURROGATE_LEAD_MAX: number = 0xdbff; +const SURROGATE_TRAIL_MIN: number = 0xdc00; +const SURROGATE_TRAIL_MAX: number = 0xdfff; + +let graphemeSegmenter: Intl.Segmenter | undefined; + +/** + * Lazily creates the `Intl.Segmenter` used for grapheme segmentation. + */ +function getGraphemeSegmenter(): Intl.Segmenter { + if (!graphemeSegmenter) { + graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); + } + + return graphemeSegmenter; +} + +/** + * Checks whether a UTF-16 code unit is a high surrogate. + */ +function isLeadSurrogate(charCode: number): boolean { + return charCode >= SURROGATE_LEAD_MIN && charCode <= SURROGATE_LEAD_MAX; +} + +/** + * Checks whether a UTF-16 code unit is a low surrogate. + */ +function isTrailSurrogate(charCode: number): boolean { + return charCode >= SURROGATE_TRAIL_MIN && charCode <= SURROGATE_TRAIL_MAX; +} + +/** + * Converts a visibility count to a non-negative integer. + * + * Fractional values are truncated toward zero. Invalid values become `0`. + */ +function toValidCount(value: number | undefined): number { + return typeof value === "number" && value > 0 && Number.isFinite(value) ? Math.trunc(value) : 0; +} + +/** + * Adjusts the start boundary forward to prevent splitting a UTF-16 surrogate pair. + */ +function adjustSurrogateStart(value: string, visibleStart: number, length: number): number { + if (visibleStart > 0 && visibleStart < length) { + const previousCode: number = value.charCodeAt(visibleStart - 1); + + if (isLeadSurrogate(previousCode)) { + const currentCode: number = value.charCodeAt(visibleStart); + + if (isTrailSurrogate(currentCode)) { + return visibleStart + 1; + } + } + } + + return visibleStart; +} + +/** + * Adjusts the end boundary backward to prevent splitting a UTF-16 surrogate pair. + */ +function adjustSurrogateEnd(value: string, visibleEnd: number, length: number): number { + if (visibleEnd > 0 && visibleEnd < length) { + const cutIndex: number = length - visibleEnd; + const currentCode: number = value.charCodeAt(cutIndex); + + if (isTrailSurrogate(currentCode)) { + const previousCode: number = value.charCodeAt(cutIndex - 1); + + if (isLeadSurrogate(previousCode)) { + return visibleEnd + 1; + } + } + } + + return visibleEnd; +} + +/** + * Returns the UTF-16 code unit length of the Unicode code point at the specified index. + */ +function getCodePointStep(value: string, charIndex: number, length: number): number { + const code: number = value.charCodeAt(charIndex); + + if (isLeadSurrogate(code) && charIndex + 1 < length) { + const nextCode: number = value.charCodeAt(charIndex + 1); + + if (isTrailSurrogate(nextCode)) { + return 2; + } + } + + return 1; +} + +/** + * Counts the total Unicode code points in a string. + */ +function countCodePoints(value: string, length: number): number { + let charIndex: number = 0; + let totalCount: number = 0; + + while (charIndex < length) { + totalCount++; + charIndex += getCodePointStep(value, charIndex, length); + } + + return totalCount; +} + +/** + * Collects the starting UTF-16 index of each Unicode code point in a string. + */ +function collectCodePointIndices(value: string, length: number): number[] { + const indices: number[] = []; + + let charIndex: number = 0; + + while (charIndex < length) { + indices.push(charIndex); + + charIndex += getCodePointStep(value, charIndex, length); + } + + return indices; +} + +/** + * Redacts a string using Unicode code point boundaries. + */ +function redactCodePoint( + value: string, + visibleStart: number, + visibleEnd: number, + replacement: string, + length: number +): string { + if (visibleStart === 0 && visibleEnd === 0) { + return replacement.repeat(countCodePoints(value, length)); + } + + const indices: number[] = collectCodePointIndices(value, length); + const totalCodePoints: number = indices.length; + + if (visibleStart + visibleEnd >= totalCodePoints) { + return value; + } + + const redactCount: number = totalCodePoints - visibleStart - visibleEnd; + + if (visibleStart > 0) { + const prefixEndIndex: number = indices[visibleStart]; + + if (visibleEnd > 0) { + return ( + value.slice(0, prefixEndIndex) + + replacement.repeat(redactCount) + + value.slice(indices[totalCodePoints - visibleEnd]) + ); + } + + return value.slice(0, prefixEndIndex) + replacement.repeat(redactCount); + } + + return replacement.repeat(redactCount) + value.slice(indices[totalCodePoints - visibleEnd]); +} + +/** + * Counts the grapheme clusters in a string. + */ +function countGraphemes(value: string): number { + const iterator: Intl.SegmentIterator = getGraphemeSegmenter().segment(value)[Symbol.iterator](); + + let totalSegments: number = 0; + + while (!iterator.next().done) { + totalSegments++; + } + + return totalSegments; +} + +/** + * Collects the UTF-16 start index of each grapheme cluster in a string. + */ +function collectGraphemeIndices(value: string): number[] { + const indices: number[] = []; + + for (const segmentData of getGraphemeSegmenter().segment(value)) { + indices.push(segmentData.index); + } + + return indices; +} + +/** + * Redacts a string using grapheme cluster boundaries. + */ +function redactGrapheme(value: string, visibleStart: number, visibleEnd: number, replacement: string): string { + if (visibleStart === 0 && visibleEnd === 0) { + return replacement.repeat(countGraphemes(value)); + } + + const indices: number[] = collectGraphemeIndices(value); + const totalSegments: number = indices.length; + + if (visibleStart + visibleEnd >= totalSegments) { + return value; + } + + const redactCount: number = totalSegments - visibleStart - visibleEnd; + + if (visibleStart > 0) { + const prefixEndIndex: number = indices[visibleStart]; + + if (visibleEnd > 0) { + return ( + value.slice(0, prefixEndIndex) + + replacement.repeat(redactCount) + + value.slice(indices[totalSegments - visibleEnd]) + ); + } + + return value.slice(0, prefixEndIndex) + replacement.repeat(redactCount); + } + + return replacement.repeat(redactCount) + value.slice(indices[totalSegments - visibleEnd]); +} + +/** + * Redacts a string using UTF-16 code units while preventing split surrogate pairs. + */ +function redactUtf16( + value: string, + visibleStart: number, + visibleEnd: number, + replacement: string, + length: number +): string { + const totalVisible: number = visibleStart + visibleEnd; + + if (totalVisible >= length) { + return value; + } + + const safeStart: number = adjustSurrogateStart(value, visibleStart, length); + const safeEnd: number = adjustSurrogateEnd(value, visibleEnd, length); + const safeTotal: number = safeStart + safeEnd; + + if (safeTotal >= length) { + return value; + } + + const redactLength: number = length - safeTotal; + + if (safeStart > 0) { + return safeEnd > 0 + ? value.slice(0, safeStart) + replacement.repeat(redactLength) + value.slice(length - safeEnd) + : value.slice(0, safeStart) + replacement.repeat(redactLength); + } + + return safeEnd > 0 ? replacement.repeat(redactLength) + value.slice(length - safeEnd) : replacement.repeat(length); +} + +/** + * Redacts every character in a string with `*`. + * + * Each character is replaced with `*`. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param value - The string to redact, or `null` or `undefined`. + * @returns The redacted string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * redact("0812345678"); // "**********" + * redact("Hello World"); // "***********" + * redact(""); // "" + * redact(null); // "" + * redact(undefined); // "" + */ +export function redact(value: string | null | undefined): string; + +/** + * Redacts every character in a string using a custom replacement string. + * + * The replacement string is repeated once for each character. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param value - The string to redact, or `null` or `undefined`. + * @param replacement - The string used to replace each redacted character. + * @returns The redacted string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * redact("0812345678", "#"); // "##########" + * redact("Hello World", "+"); // "+++++++++++" + * redact("12345", "XX"); // "XXXXXXXXXX" + * redact("", "#"); // "" + * redact(null, "#"); // "" + * redact(undefined, "#"); // "" + */ +export function redact(value: string | null | undefined, replacement: string): string; + +/** + * Redacts a string while preserving a specified number of characters at the beginning. + * + * Preserves the first `visibleStart` characters and replaces the rest with `*`. + * Non-finite, negative, and zero values default to `0`. + * Fractional values are truncated toward zero. + * + * Surrogate pairs at the boundary are preserved without splitting. + * + * @param value - The string to redact, or `null` or `undefined`. + * @param visibleStart - The number of characters to keep visible at the beginning. + * @returns The partially redacted string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * redact("0812345678", 2); // "08********" + * redact("0812345678", 4); // "0812******" + * redact("0812345678", 10); // "0812345678" + * redact("0812345678", -1); // "**********" + * redact("0812345678", 2.9); // "08********" + * redact("", 2); // "" + * redact(null, 2); // "" + * redact(undefined, 2); // "" + */ +export function redact(value: string | null | undefined, visibleStart: number): string; + +/** + * Redacts a string while preserving the specified number of characters at the beginning and end. + * + * The `mode` option determines how characters are counted. + * + * Returns the original string when all characters are visible. + * Returns an empty string when `value` is empty, `null`, or `undefined`. + * + * @param value - The string to redact, or `null` or `undefined`. + * @param options - Redaction options. + * @returns The redacted string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * redact("0812345678", { visibleStart: 3, visibleEnd: 2 }); // "081*****78" + * redact("0812345678", { visibleStart: 2, replacement: "#" }); // "08########" + * redact("0812345678", { visibleEnd: 2, replacement: "#" }); // "########78" + * redact("1234567890", { visibleStart: 4, visibleEnd: 4, replacement: "X" }); // "1234XX7890" + * redact("😀😀😀😀", { visibleStart: 1, mode: "codePoint" }); // "😀***" + * redact("👨‍👩‍👧‍👦👨‍👩‍👧‍👦", { visibleStart: 1, mode: "grapheme" }); // "👨‍👩‍👧‍👦*" + * redact("", { visibleStart: 2 }); // "" + * redact(null, { visibleStart: 2 }); // "" + * redact(undefined, { visibleStart: 2 }); // "" + */ +export function redact(value: string | null | undefined, options: RedactOptions): string; + +export function redact(value: string | null | undefined, options?: string | number | RedactOptions): string { + if (!hasLength(value)) { + return EMPTY; + } + + const length: number = value.length; + + if (options === undefined) { + return ASTERISK.repeat(length); + } + + if (typeof options === "string") { + return options.repeat(length); + } + + if (typeof options === "number") { + let visibleStart: number = toValidCount(options); + + if (visibleStart >= length) { + return value; + } + + if (visibleStart <= 0) { + return ASTERISK.repeat(length); + } + + visibleStart = adjustSurrogateStart(value, visibleStart, length); + + return value.slice(0, visibleStart) + ASTERISK.repeat(length - visibleStart); + } + + const { visibleStart: startOpt, visibleEnd: endOpt, replacement = ASTERISK, mode = "utf16" }: RedactOptions = options; + const visibleStart: number = toValidCount(startOpt); + const visibleEnd: number = toValidCount(endOpt); + + if (mode === "grapheme") { + return redactGrapheme(value, visibleStart, visibleEnd, replacement); + } + + if (mode === "codePoint") { + return redactCodePoint(value, visibleStart, visibleEnd, replacement, length); + } + + return redactUtf16(value, visibleStart, visibleEnd, replacement, length); +} diff --git a/src/libs/slugify/index.ts b/src/libs/slugify/index.ts new file mode 100644 index 0000000..b7107c0 --- /dev/null +++ b/src/libs/slugify/index.ts @@ -0,0 +1 @@ +export * from "./slugify.lib"; diff --git a/src/libs/slugify/slugify.lib.bench.ts b/src/libs/slugify/slugify.lib.bench.ts new file mode 100644 index 0000000..0944cf8 --- /dev/null +++ b/src/libs/slugify/slugify.lib.bench.ts @@ -0,0 +1,141 @@ +import { describe, test, TestContext } from "vitest"; + +import { slugify } from "./slugify.lib"; + +const longSlugText: string = "Get HTTP Response Code -- Version 2.0 API & More! ".repeat(100); +const longUnicodeText: string = "Crème Brûlée -- Привет, мир! -- 你好 世界 -- สวัสดี โลก ".repeat(100); +const longSeparatorsText: string = "---foo---bar___baz...qux!!!$$$%%%".repeat(100); +const longSymbols: string = "!@#$%^&*()_+-=[]{}|;:',.<>?/`~".repeat(100); +const longWhitespace: string = " ".repeat(1000); + +describe("slugify", (): void => { + test("given common short ASCII inputs", ({ bench }: TestContext): void => { + bench("simple two-word phrase", (): void => { + slugify("Hello World"); + }); + + bench("with custom separator", (): void => { + slugify("Hello World", { separator: "_" }); + }); + + bench("with lower: false", (): void => { + slugify("Hello World", { lower: false }); + }); + + bench("already a valid slug (no-op path)", (): void => { + slugify("hello-world"); + }); + }); + + test("given numbers and acronyms", ({ bench }: TestContext): void => { + bench("sentence with acronyms and version numbers", (): void => { + slugify("Get HTTP Response Code -- Version 2.0 API & More!"); + }); + + bench("numeric-only input", (): void => { + slugify("123456789"); + }); + + bench("word with numbers", (): void => { + slugify("version-2-0"); + }); + }); + + test("given long inputs", ({ bench }: TestContext): void => { + bench("long text with acronyms and symbols", (): void => { + slugify(longSlugText); + }); + + bench("long text with custom separator and no lowercase", (): void => { + slugify(longSlugText, { separator: "_", lower: false }); + }); + + bench("long Unicode text", (): void => { + slugify(longUnicodeText); + }); + + bench("empty separator (concatenate words)", (): void => { + slugify(longSlugText, { separator: "" }); + }); + }); + + test("given Unicode and combining marks", ({ bench }: TestContext): void => { + bench("accented Latin (NFD normalization)", (): void => { + slugify("Crème Brûlée"); + }); + + bench("accented Latin with lower: false", (): void => { + slugify("Crème Brûlée", { lower: false }); + }); + + bench("mixed accented and ASCII", (): void => { + slugify("café au lait & crêpes"); + }); + + bench("combining diacritical marks", (): void => { + slugify("resume\u0301 naïve"); + }); + + bench("CJK characters", (): void => { + slugify("你好 世界 -- こんにちは 世界 -- 안녕하세요 세계"); + }); + + bench("Cyrillic script", (): void => { + slugify("Привет, мир! -- Быстрая коричневая лиса"); + }); + + bench("Thai script with combining vowels/tones", (): void => { + slugify("สวัสดี โลก ภาษาไทย 101"); + }); + + bench("Arabic script", (): void => { + slugify("مرحبا بالعالم -- أهلا و سهلا"); + }); + }); + + test("given excessive separators", ({ bench }: TestContext): void => { + bench("leading and trailing separators", (): void => { + slugify("---Hello World---"); + }); + + bench("consecutive separators", (): void => { + slugify("foo!!!@@@###bar$$$%%%baz"); + }); + + bench("mixed whitespace and punctuation padding", (): void => { + slugify(" -- foo __ bar .. baz -- "); + }); + + bench("long separator-heavy input", (): void => { + slugify(longSeparatorsText); + }); + }); + + test("given non-extractable input", ({ bench }: TestContext): void => { + bench("symbol-only input", (): void => { + slugify("!@#$%^&*()_+-=[]{}"); + }); + + bench("long symbol-only input", (): void => { + slugify(longSymbols); + }); + + bench("long whitespace-only input", (): void => { + slugify(longWhitespace); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + slugify(""); + }); + + bench("null", (): void => { + slugify(null); + }); + + bench("undefined", (): void => { + slugify(undefined); + }); + }); +}); diff --git a/src/libs/slugify/slugify.lib.spec.ts b/src/libs/slugify/slugify.lib.spec.ts new file mode 100644 index 0000000..9064d22 --- /dev/null +++ b/src/libs/slugify/slugify.lib.spec.ts @@ -0,0 +1,279 @@ +import { describe, expect, test } from "vitest"; + +import { slugify } from "./slugify.lib"; + +describe("slugify", (): void => { + describe("given a string containing standard text", (): void => { + test("converts text to a lowercase slug", (): void => { + expect(slugify("Hello World")).toBe("hello-world"); + expect(slugify("Hello World!")).toBe("hello-world"); + expect(slugify("Hello World 123")).toBe("hello-world-123"); + }); + + test("preserves alphanumeric characters", (): void => { + expect(slugify("Hello123")).toBe("hello123"); + expect(slugify("12345")).toBe("12345"); + expect(slugify("Version 2.0")).toBe("version-2-0"); + }); + + test("collapses consecutive non-alphanumeric characters into one separator", (): void => { + expect(slugify("Hello World")).toBe("hello-world"); + expect(slugify("Hello---World")).toBe("hello-world"); + expect(slugify("Hello...World")).toBe("hello-world"); + expect(slugify("Hello & World")).toBe("hello-world"); + expect(slugify("Hello - & - World")).toBe("hello-world"); + }); + + test("trims leading and trailing separators", (): void => { + expect(slugify(" Hello World ")).toBe("hello-world"); + expect(slugify("---Hello World---")).toBe("hello-world"); + expect(slugify("...Hello World...")).toBe("hello-world"); + expect(slugify("!!!Hello World!!!")).toBe("hello-world"); + }); + }); + + describe("given accented and unicode characters", (): void => { + test("normalizes accented Latin characters", (): void => { + expect(slugify("Crème Brûlée")).toBe("creme-brulee"); + expect(slugify("café")).toBe("cafe"); + expect(slugify("naïve")).toBe("naive"); + expect(slugify("résumé")).toBe("resume"); + }); + + test("normalizes uppercase accented characters before lowercasing", (): void => { + expect(slugify("ÉLÈVE À PARIS")).toBe("eleve-a-paris"); + expect(slugify("À Propos")).toBe("a-propos"); + }); + + test("removes combining diacritical marks from Latin characters", (): void => { + expect(slugify("e\u0301")).toBe("e"); + expect(slugify("Cafe\u0301")).toBe("cafe"); + }); + + test("supports CJK characters (Chinese, Japanese, Korean)", (): void => { + expect(slugify("Hello 世界")).toBe("hello-世界"); + expect(slugify("你好 世界")).toBe("你好-世界"); + expect(slugify("中文")).toBe("中文"); + expect(slugify("こんにちは 世界")).toBe("こんにちは-世界"); + expect(slugify("カタカナ テスト")).toBe("カタカナ-テスト"); + expect(slugify("안녕하세요 세계")).toBe("안녕하세요-세계"); + }); + + test("supports Cyrillic characters", (): void => { + expect(slugify("Привет, мир!")).toBe("привет-мир"); + expect(slugify("Быстрая лиса")).toBe("быстрая-лиса"); + }); + + test("supports Arabic characters", (): void => { + expect(slugify("مرحبا بالعالم")).toBe("مرحبا-بالعالم"); + expect(slugify("أهلا و سهلا")).toBe("أهلا-و-سهلا"); + }); + + test("supports Thai characters and combining marks", (): void => { + expect(slugify("สวัสดี โลก")).toBe("สวัสดี-โลก"); + expect(slugify("ภาษาไทย 101")).toBe("ภาษาไทย-101"); + }); + + test("supports Greek, Hebrew, and Devanagari scripts", (): void => { + expect(slugify("Γειά σου Κόσμε")).toBe("γεια-σου-κοσμε"); + expect(slugify("שלום עולם")).toBe("שלום-עולם"); + expect(slugify("नमस्ते दुनिया")).toBe("नमस्ते-दुनिया"); + }); + }); + + describe("given a string with whitespace", (): void => { + test("converts whitespace between words into separators", (): void => { + expect(slugify("Hello World")).toBe("hello-world"); + expect(slugify("Hello\tWorld")).toBe("hello-world"); + expect(slugify("Hello\nWorld")).toBe("hello-world"); + expect(slugify("Hello\r\nWorld")).toBe("hello-world"); + }); + + test("handles unicode whitespace", (): void => { + expect(slugify("Hello\u00A0World")).toBe("hello-world"); + expect(slugify("Hello\u2003World")).toBe("hello-world"); + expect(slugify("Hello\u3000World")).toBe("hello-world"); + }); + + test("returns an empty string for whitespace-only input", (): void => { + expect(slugify(" ")).toBe(""); + expect(slugify(" ")).toBe(""); + expect(slugify("\t\n\r")).toBe(""); + expect(slugify("\u00A0\u2003\u3000")).toBe(""); + }); + }); + + describe("given the lower option", (): void => { + test("converts the result to lowercase by default", (): void => { + expect(slugify("Hello WORLD")).toBe("hello-world"); + expect(slugify("ПРИВЕТ МИР")).toBe("привет-мир"); + }); + + test("preserves letter casing when lower is false", (): void => { + expect(slugify("Hello WORLD", { lower: false })).toBe("Hello-WORLD"); + expect(slugify("Crème Brûlée", { lower: false })).toBe("Creme-Brulee"); + expect(slugify("Привет, МИР!", { lower: false })).toBe("Привет-МИР"); + }); + + test("converts the result to lowercase when lower is true", (): void => { + expect(slugify("Hello WORLD", { lower: true })).toBe("hello-world"); + expect(slugify("Привет МИР", { lower: true })).toBe("привет-мир"); + }); + }); + + describe("given the separator option", (): void => { + test("uses a custom separator", (): void => { + expect(slugify("Hello World", { separator: "_" })).toBe("hello_world"); + expect(slugify("Hello World", { separator: "." })).toBe("hello.world"); + expect(slugify("Hello World", { separator: "~" })).toBe("hello~world"); + expect(slugify("你好 世界", { separator: "_" })).toBe("你好_世界"); + }); + + test("supports multi-character separators", (): void => { + expect(slugify("Hello World", { separator: "--" })).toBe("hello--world"); + expect(slugify("Hello World", { separator: "__" })).toBe("hello__world"); + expect(slugify("Hello World", { separator: "..." })).toBe("hello...world"); + expect(slugify("สวัสดี โลก", { separator: "--" })).toBe("สวัสดี--โลก"); + }); + + test("trims repeated custom separators from the boundaries", (): void => { + expect(slugify(" Hello World ", { separator: "--" })).toBe("hello--world"); + expect(slugify("Hello World", { separator: "__" })).toBe("hello__world"); + expect(slugify("---Hello World---", { separator: "--" })).toBe("hello--world"); + }); + + test("removes non-alphanumeric characters when the separator is empty", (): void => { + expect(slugify("Hello, World!", { separator: "" })).toBe("helloworld"); + expect(slugify("Foo & Bar", { separator: "" })).toBe("foobar"); + expect(slugify("你好, 世界!", { separator: "" })).toBe("你好世界"); + expect(slugify("สวัสดี, โลก!", { separator: "" })).toBe("สวัสดีโลก"); + }); + + test("preserves letter casing when the separator is empty and lower is false", (): void => { + expect( + slugify("Hello, World!", { + separator: "", + lower: false + }) + ).toBe("HelloWorld"); + + expect( + slugify("Foo & Bar", { + separator: "", + lower: false + }) + ).toBe("FooBar"); + + expect( + slugify("Привет, Мир!", { + separator: "", + lower: false + }) + ).toBe("ПриветМир"); + }); + }); + + describe("given special replacement-token strings as separators", (): void => { + test("preserves $& as a literal separator", (): void => { + expect(slugify("Hello World", { separator: "$&" })).toBe("hello$&world"); + }); + + test("preserves $1 as a literal separator", (): void => { + expect(slugify("Hello World", { separator: "$1" })).toBe("hello$1world"); + }); + + test("preserves $` as a literal separator", (): void => { + expect(slugify("Hello World", { separator: "$`" })).toBe("hello$`world"); + }); + + test("preserves $' as a literal separator", (): void => { + expect(slugify("Hello World", { separator: "$'" })).toBe("hello$'world"); + }); + }); + + describe("given separators containing regular expression characters", (): void => { + test("supports regex metacharacters as separators", (): void => { + expect(slugify("Hello World", { separator: "." })).toBe("hello.world"); + expect(slugify("Hello World", { separator: "+" })).toBe("hello+world"); + expect(slugify("Hello World", { separator: "*" })).toBe("hello*world"); + expect(slugify("Hello World", { separator: "?" })).toBe("hello?world"); + expect(slugify("Hello World", { separator: "[" })).toBe("hello[world"); + expect(slugify("Hello World", { separator: "[]" })).toBe("hello[]world"); + }); + }); + + describe("given punctuation and symbols", (): void => { + test("replaces punctuation with separators", (): void => { + expect(slugify("Hello, World!")).toBe("hello-world"); + expect(slugify("Hello: World;")).toBe("hello-world"); + expect(slugify("Hello / World")).toBe("hello-world"); + expect(slugify("Hello @ World")).toBe("hello-world"); + expect(slugify("Hello (World) [123]")).toBe("hello-world-123"); + }); + + test("replaces emoji with separators", (): void => { + expect(slugify("Hello 🔥 World")).toBe("hello-world"); + expect(slugify("🔥 Hello World 🔥")).toBe("hello-world"); + expect(slugify("Hello🔥World")).toBe("hello-world"); + }); + + test("returns an empty string when the input contains only symbols, punctuation, or emoji", (): void => { + expect(slugify("🔥")).toBe(""); + expect(slugify("!!!")).toBe(""); + expect(slugify("---")).toBe(""); + expect(slugify("@#$%^&*()")).toBe(""); + }); + }); + + describe("given already slugified text", (): void => { + test("preserves an existing valid slug", (): void => { + expect(slugify("hello-world")).toBe("hello-world"); + expect(slugify("你好-世界")).toBe("你好-世界"); + }); + + test("normalizes an invalid slug", (): void => { + expect(slugify("Hello--World")).toBe("hello-world"); + expect(slugify("hello___world")).toBe("hello-world"); + expect(slugify("--hello--world--")).toBe("hello-world"); + }); + }); + + describe("given a slugified result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "Hello World", + "Foo & Bar", + "Crème Brûlée", + "Привет, мир!", + "你好 世界", + "สวัสดี โลก", + "Version 2.0", + "hello-world" + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = slugify(input); + + expect(slugify(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(slugify("")).toBe(""); + expect(slugify("", { separator: "_" })).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(slugify(null)).toBe(""); + expect(slugify(null, { separator: "_" })).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(slugify(undefined)).toBe(""); + expect(slugify(undefined, { separator: "_" })).toBe(""); + }); + }); +}); diff --git a/src/libs/slugify/slugify.lib.ts b/src/libs/slugify/slugify.lib.ts new file mode 100644 index 0000000..9959b92 --- /dev/null +++ b/src/libs/slugify/slugify.lib.ts @@ -0,0 +1,141 @@ +import { DIACRITICS_REGEX, DOLLAR_SIGN, EMPTY, HYPHEN, NON_ALPHANUMERIC_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Configuration options for {@link slugify}. + */ +export interface SlugifyOptions { + /** + * String used to separate words in the slug. + * + * @default "-" + */ + separator?: string; + + /** + * Whether to convert the output string to lowercase. + * + * @default true + */ + lower?: boolean; +} + +/** + * Trims leading and trailing occurrences of a separator. + * + * @param value - The string to trim. + * @param separator - The separator to remove from the boundaries. + * @returns The trimmed string. + */ +function trimSeparator(value: string, separator: string): string { + if (!value || !separator) { + return value; + } + + const separatorLength: number = separator.length; + + let startIndex: number = 0; + let endIndex: number = value.length; + + while (value.startsWith(separator, startIndex)) { + startIndex += separatorLength; + } + + while (endIndex > startIndex && value.endsWith(separator, endIndex)) { + endIndex -= separatorLength; + } + + return startIndex === 0 && endIndex === value.length ? value : value.slice(startIndex, endIndex); +} + +/** + * Converts a string into a URL-friendly slug using the default options. + * + * Latin diacritics and accented characters are normalized to their base + * characters, while Unicode letters, combining marks, and numbers are + * preserved. + * + * Non-alphanumeric characters are replaced with `-`, consecutive separators + * are collapsed into a single separator, and leading and trailing separators + * are removed. + * + * The resulting slug is converted to lowercase by default. + * + * @param value - The string to transform into a slug, or `null` or `undefined`. + * @returns The slugified string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * slugify("Hello World"); // "hello-world" + * slugify("Foo & Bar"); // "foo-bar" + * slugify("Crème Brûlée"); // "creme-brulee" + * slugify("Привет, мир!"); // "привет-мир" + * slugify("你好 世界"); // "你好-世界" + * slugify("สวัสดี โลก"); // "สวัสดี-โลก" + * slugify("Version 2.0"); // "version-2-0" + * slugify(""); // "" + * slugify(null); // "" + * slugify(undefined); // "" + */ +export function slugify(value: string | null | undefined): string; + +/** + * Converts a string into a URL-friendly slug using the specified options. + * + * Latin diacritics and accented characters are normalized to their base + * characters, while Unicode letters, combining marks, and numbers are + * preserved. + * + * Non-alphanumeric characters are replaced with the configured separator, + * consecutive separators are collapsed into a single separator, and leading + * and trailing separators are removed. + * + * By default, the resulting slug is converted to lowercase and uses `-` as + * the separator. These defaults can be overridden through `options`. + * + * An empty separator removes non-alphanumeric characters instead of replacing + * them with a separator. Separators containing `$` are treated as literal + * strings rather than JavaScript replacement patterns. + * + * @param value - The string to transform into a slug, or `null` or `undefined`. + * @param options - Configuration options for the separator and letter casing. + * @returns The slugified string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * slugify("Hello World", { separator: "_" }); // "hello_world" + * slugify("Hello World", { separator: "." }); // "hello.world" + * slugify("Hello World", { separator: "--" }); // "hello--world" + * slugify("Hello World", { separator: "$&" }); // "hello$&world" + * slugify("Hello World", { lower: false }); // "Hello-World" + * slugify("Crème Brûlée", { lower: false }); // "Creme-Brulee" + * slugify("Привет, МИР!", { lower: false }); // "Привет-МИР" + * slugify("你好 世界", { separator: "_" }); // "你好_世界" + * slugify("สวัสดี โลก", { separator: "" }); // "สวัสดีโลก" + * slugify("Hello World", { separator: "", lower: false }); // "HelloWorld" + * slugify("", { separator: "_" }); // "" + * slugify(null, { separator: "_" }); // "" + * slugify(undefined, { separator: "_" }); // "" + */ +export function slugify(value: string | null | undefined, options: SlugifyOptions): string; + +export function slugify(value: string | null | undefined, options?: SlugifyOptions): string { + if (!hasLength(value)) { + return EMPTY; + } + + const { separator = HYPHEN, lower = true }: SlugifyOptions = options ?? {}; + + let result: string = value.normalize("NFD").replace(DIACRITICS_REGEX, EMPTY).normalize("NFC"); + + if (lower) { + result = result.toLowerCase(); + } + + result = separator.includes(DOLLAR_SIGN) + ? result.replace(NON_ALPHANUMERIC_REGEX, (): string => separator) + : result.replace(NON_ALPHANUMERIC_REGEX, separator); + + return trimSeparator(result, separator); +} diff --git a/src/libs/snake-case/index.ts b/src/libs/snake-case/index.ts new file mode 100644 index 0000000..0ed0148 --- /dev/null +++ b/src/libs/snake-case/index.ts @@ -0,0 +1 @@ +export * from "./snake-case.lib"; diff --git a/src/libs/snake-case/snake-case.lib.bench.ts b/src/libs/snake-case/snake-case.lib.bench.ts new file mode 100644 index 0000000..268db77 --- /dev/null +++ b/src/libs/snake-case/snake-case.lib.bench.ts @@ -0,0 +1,115 @@ +import { describe, test, TestContext } from "vitest"; + +import { snakeCase } from "./snake-case.lib"; + +const shortPascalCase: string = "XMLHttpRequestResponseCodeHandler"; +const input1KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(20); +const input10KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(200); +const input100KB: string = "get-HTTP-response-code-version-2.0-API ".repeat(2000); +const longUnicodeInput: string = "Привет-Мир café-noir 你好-世界 こんにちは-世界 مرحبا-بالعالم ".repeat(100); +const separatorHeavyInput: string = "---foo---bar___baz...qux!!!".repeat(1000); +const mixedCaseInput: string = "aAaAaAaAaAaAaAaAaAaAaAaAaAaAaA".repeat(1000); + +describe("snakeCase", (): void => { + test("given common short ASCII inputs", ({ bench }: TestContext): void => { + bench("hyphen-separated lowercase", (): void => { + snakeCase("foo-bar"); + }); + + bench("underscore-separated uppercase", (): void => { + snakeCase("FOO_BAR"); + }); + + bench("camelCase", (): void => { + snakeCase("fooBar"); + }); + + bench("already snake_case", (): void => { + snakeCase("foo_bar"); + }); + + bench("PascalCase", (): void => { + snakeCase(shortPascalCase); + }); + + bench("acronym followed by word", (): void => { + snakeCase("HTTPResponseCode"); + }); + + bench("mixed words and numbers", (): void => { + snakeCase("version-2-0"); + }); + }); + + test("given inputs of increasing size", ({ bench }: TestContext): void => { + bench("approximately 1 KB input", (): void => { + snakeCase(input1KB); + }); + + bench("approximately 10 KB input", (): void => { + snakeCase(input10KB); + }); + + bench("approximately 100 KB input", (): void => { + snakeCase(input100KB); + }); + }); + + test("given Unicode input", ({ bench }: TestContext): void => { + bench("long Unicode input", (): void => { + snakeCase(longUnicodeInput); + }); + + bench("combining marks", (): void => { + snakeCase("cafe\u0301 noir"); + }); + + bench("Cyrillic", (): void => { + snakeCase("Привет Мир"); + }); + + bench("CJK", (): void => { + snakeCase("你好 世界"); + }); + + bench("Arabic", (): void => { + snakeCase("مرحبا بالعالم"); + }); + }); + + test("given separator-heavy input", ({ bench }: TestContext): void => { + bench("long separator-heavy input", (): void => { + snakeCase(separatorHeavyInput); + }); + }); + + test("given mixed-case input", ({ bench }: TestContext): void => { + bench("long mixed-case input", (): void => { + snakeCase(mixedCaseInput); + }); + }); + + test("given simple inputs", ({ bench }: TestContext): void => { + bench("single word", (): void => { + snakeCase("hello"); + }); + + bench("numeric-only input", (): void => { + snakeCase("123456789"); + }); + }); + + test("given an empty or nullish value", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + snakeCase(""); + }); + + bench("null", (): void => { + snakeCase(null); + }); + + bench("undefined", (): void => { + snakeCase(undefined); + }); + }); +}); diff --git a/src/libs/snake-case/snake-case.lib.spec.ts b/src/libs/snake-case/snake-case.lib.spec.ts new file mode 100644 index 0000000..3c1a119 --- /dev/null +++ b/src/libs/snake-case/snake-case.lib.spec.ts @@ -0,0 +1,174 @@ +import { describe, expect, test } from "vitest"; + +import { snakeCase } from "./snake-case.lib"; + +describe("snakeCase", (): void => { + describe("given a standard string with separators", (): void => { + test("returns the snake_case string", (): void => { + expect(snakeCase("foo-bar")).toBe("foo_bar"); + expect(snakeCase("foo_bar")).toBe("foo_bar"); + expect(snakeCase("foo.bar")).toBe("foo_bar"); + expect(snakeCase("foo bar")).toBe("foo_bar"); + expect(snakeCase("--foo-bar--")).toBe("foo_bar"); + expect(snakeCase("__foo_bar__")).toBe("foo_bar"); + expect(snakeCase(" foo bar ")).toBe("foo_bar"); + }); + + test("handles mixed separators", (): void => { + expect(snakeCase("foo-bar_baz.qux")).toBe("foo_bar_baz_qux"); + expect(snakeCase("foo / bar \\ baz")).toBe("foo_bar_baz"); + expect(snakeCase("foo!!!bar")).toBe("foo_bar"); + expect(snakeCase("foo---bar")).toBe("foo_bar"); + expect(snakeCase("foo...bar")).toBe("foo_bar"); + }); + }); + + describe("given an uppercase string with separators", (): void => { + test("returns the snake_case string", (): void => { + expect(snakeCase("FOO-BAR")).toBe("foo_bar"); + expect(snakeCase("FOO_BAR")).toBe("foo_bar"); + expect(snakeCase("FOO BAR")).toBe("foo_bar"); + expect(snakeCase("FOO-BAR-BAZ")).toBe("foo_bar_baz"); + }); + }); + + describe("given a camelCase string", (): void => { + test("returns the snake_case string", (): void => { + expect(snakeCase("fooBar")).toBe("foo_bar"); + expect(snakeCase("fooBarBaz")).toBe("foo_bar_baz"); + expect(snakeCase("xmlHttpRequest")).toBe("xml_http_request"); + }); + }); + + describe("given a PascalCase string", (): void => { + test("returns the snake_case string", (): void => { + expect(snakeCase("FooBar")).toBe("foo_bar"); + expect(snakeCase("FooBarBaz")).toBe("foo_bar_baz"); + expect(snakeCase("XmlHttpRequest")).toBe("xml_http_request"); + }); + }); + + describe("given acronyms and mixed casing", (): void => { + test("handles consecutive uppercase letters", (): void => { + expect(snakeCase("HTTPResponseCode")).toBe("http_response_code"); + expect(snakeCase("JSONDataAPI")).toBe("json_data_api"); + expect(snakeCase("XMLHttpRequest")).toBe("xml_http_request"); + expect(snakeCase("parseHTTPResponse")).toBe("parse_http_response"); + expect(snakeCase("getURLValue")).toBe("get_url_value"); + }); + }); + + describe("given strings containing numbers", (): void => { + test("handles numbers as separate words", (): void => { + expect(snakeCase("version 2.0")).toBe("version_2_0"); + expect(snakeCase("version-2-0")).toBe("version_2_0"); + expect(snakeCase("version_2_0")).toBe("version_2_0"); + expect(snakeCase("2foo")).toBe("2_foo"); + expect(snakeCase("foo2bar")).toBe("foo_2_bar"); + }); + + test("preserves numeric-only input", (): void => { + expect(snakeCase("123")).toBe("123"); + expect(snakeCase("123-456")).toBe("123_456"); + expect(snakeCase("123_456_789")).toBe("123_456_789"); + }); + }); + + describe("given strings with accents and Unicode", (): void => { + test("handles accented letters as part of words", (): void => { + expect(snakeCase("crème-brûlée")).toBe("crème_brûlée"); + expect(snakeCase("jalapeño popper")).toBe("jalapeño_popper"); + expect(snakeCase("École Normale")).toBe("école_normale"); + }); + + test("handles Cyrillic casing", (): void => { + expect(snakeCase("Привет Мир")).toBe("привет_мир"); + expect(snakeCase("привет-мир")).toBe("привет_мир"); + expect(snakeCase("ПРИВЕТ МИР")).toBe("привет_мир"); + }); + + test("preserves combining marks", (): void => { + expect(snakeCase("cafe\u0301 noir")).toBe("cafe\u0301_noir"); + expect(snakeCase("Cafe\u0301 Noir")).toBe("cafe\u0301_noir"); + }); + }); + + describe("given non-cased scripts", (): void => { + test("joins CJK, Hiragana, and Hangul without case transformation", (): void => { + expect(snakeCase("こんにちは 世界")).toBe("こんにちは_世界"); + expect(snakeCase("你好 世界")).toBe("你好_世界"); + expect(snakeCase("안녕하세요 세계")).toBe("안녕하세요_세계"); + }); + + test("joins Arabic words without case transformation", (): void => { + expect(snakeCase("مرحبا بالعالم")).toBe("مرحبا_بالعالم"); + }); + + test("handles mixed Latin and non-cased scripts", (): void => { + expect(snakeCase("hello 世界")).toBe("hello_世界"); + expect(snakeCase("Hello 世界")).toBe("hello_世界"); + expect(snakeCase("foo-こんにちは")).toBe("foo_こんにちは"); + expect(snakeCase("hello 世界 Привет")).toBe("hello_世界_привет"); + }); + }); + + describe("given strings containing only symbols or separators", (): void => { + test("returns an empty string when no words are extractable", (): void => { + expect(snakeCase("---")).toBe(""); + expect(snakeCase("___")).toBe(""); + expect(snakeCase("...")).toBe(""); + expect(snakeCase("!!!")).toBe(""); + expect(snakeCase("@#$%^&*")).toBe(""); + }); + }); + + describe("given a single word", (): void => { + test("returns the normalized word without adding separators", (): void => { + expect(snakeCase("hello")).toBe("hello"); + expect(snakeCase("Hello")).toBe("hello"); + expect(snakeCase("HELLO")).toBe("hello"); + }); + + test("preserves a numeric-only word", (): void => { + expect(snakeCase("123456789")).toBe("123456789"); + }); + }); + + describe("given a snake_case result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = [ + "foo-bar", + "FOO_BAR", + "fooBar", + "FooBar", + "HTTPResponseCode", + "JSONDataAPI", + "version-2-0", + "crème-brûlée", + "Привет Мир", + "hello 世界" + ]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = snakeCase(input); + + expect(snakeCase(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(snakeCase("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(snakeCase(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(snakeCase(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/snake-case/snake-case.lib.ts b/src/libs/snake-case/snake-case.lib.ts new file mode 100644 index 0000000..6745b1e --- /dev/null +++ b/src/libs/snake-case/snake-case.lib.ts @@ -0,0 +1,48 @@ +import { EMPTY, UNDERSCORE, WORD_REGEX } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Converts a string to snake_case. + * + * Handles separators, existing casing styles, acronyms, numbers, Unicode characters, and combining marks. + * + * @param value - The string to convert to snake_case, or `null` or `undefined`. + * @returns The snake_case string, or an empty string when `value` is empty, `null`, or `undefined`, or contains no extractable words. + * + * @example + * + * snakeCase("Foo Bar"); // "foo_bar" + * snakeCase("--foo-bar--"); // "foo_bar" + * snakeCase("FOO_BAR"); // "foo_bar" + * snakeCase("fooBar"); // "foo_bar" + * snakeCase("XMLHttpRequest"); // "xml_http_request" + * snakeCase("version 2.0"); // "version_2_0" + * snakeCase("Привет Мир"); // "привет_мир" + * snakeCase("你好 世界"); // "你好_世界" + * snakeCase("cafe\u0301 noir"); // "cafe\u0301_noir" + * snakeCase(""); // "" + * snakeCase(null); // "" + * snakeCase(undefined); // "" + */ +export function snakeCase(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const words: RegExpMatchArray | null = value.match(WORD_REGEX); + + if (!words) { + return EMPTY; + } + + const length: number = words.length; + + let result: string = words[0].toLowerCase(); + + for (let index: number = 1; index < length; index++) { + result += UNDERSCORE + words[index].toLowerCase(); + } + + return result; +} diff --git a/src/libs/uncapitalize/index.ts b/src/libs/uncapitalize/index.ts new file mode 100644 index 0000000..7229916 --- /dev/null +++ b/src/libs/uncapitalize/index.ts @@ -0,0 +1 @@ +export * from "./uncapitalize.lib"; diff --git a/src/libs/uncapitalize/uncapitalize.lib.bench.ts b/src/libs/uncapitalize/uncapitalize.lib.bench.ts new file mode 100644 index 0000000..66c8d7b --- /dev/null +++ b/src/libs/uncapitalize/uncapitalize.lib.bench.ts @@ -0,0 +1,135 @@ +import { describe, test, TestContext } from "vitest"; + +import { uncapitalize } from "./uncapitalize.lib"; + +const longCapitalizedString: string = "The quick brown fox jumps over the lazy dog ".repeat(100); +const longLowercaseString: string = "the quick brown fox jumps over the lazy dog ".repeat(100); +const longPascalCaseString: string = "XmlHttpRequestResponseCodeHandler".repeat(100); +const longSymbols: string = "!@#$%^&*()".repeat(1000); +const longWhitespace: string = " ".repeat(1000); + +describe("uncapitalize", (): void => { + test("short ASCII", ({ bench }: TestContext): void => { + bench("uppercase first character", (): void => { + uncapitalize("Hello"); + }); + + bench("lowercase first character (no-op)", (): void => { + uncapitalize("hello"); + }); + + bench("single uppercase character", (): void => { + uncapitalize("A"); + }); + + bench("single lowercase character (no-op)", (): void => { + uncapitalize("a"); + }); + }); + + test("long ASCII", ({ bench }: TestContext): void => { + bench("uppercase first character", (): void => { + uncapitalize(longCapitalizedString); + }); + + bench("lowercase first character (no-op)", (): void => { + uncapitalize(longLowercaseString); + }); + + bench("PascalCase", (): void => { + uncapitalize(longPascalCaseString); + }); + }); + + test("Unicode", ({ bench }: TestContext): void => { + bench("Latin uppercase first character", (): void => { + uncapitalize("Élève"); + }); + + bench("Latin lowercase first character (no-op)", (): void => { + uncapitalize("élève"); + }); + + bench("Cyrillic uppercase first character", (): void => { + uncapitalize("Привет Мир"); + }); + + bench("Cyrillic lowercase first character (no-op)", (): void => { + uncapitalize("привет мир"); + }); + + bench("CJK (no-op)", (): void => { + uncapitalize("你好世界"); + }); + + bench("Thai (no-op)", (): void => { + uncapitalize("สวัสดี"); + }); + }); + + test("non-letter first character", ({ bench }: TestContext): void => { + bench("numbers", (): void => { + uncapitalize("123Hello"); + }); + + bench("underscore", (): void => { + uncapitalize("_Hello"); + }); + + bench("symbol", (): void => { + uncapitalize("@Hello"); + }); + + bench("emoji", (): void => { + uncapitalize("🔥Hello"); + }); + }); + + test("leading whitespace", ({ bench }: TestContext): void => { + bench("spaces", (): void => { + uncapitalize(" Hello World"); + }); + + bench("tabs", (): void => { + uncapitalize("\tHello World"); + }); + + bench("newlines", (): void => { + uncapitalize("\nHello World"); + }); + }); + + test("leading separators", ({ bench }: TestContext): void => { + bench("hyphens", (): void => { + uncapitalize("---Hello World"); + }); + + bench("symbols", (): void => { + uncapitalize("!@#$%^&*()Hello"); + }); + }); + + test("long no-op strings", ({ bench }: TestContext): void => { + bench("symbols only", (): void => { + uncapitalize(longSymbols); + }); + + bench("whitespace only", (): void => { + uncapitalize(longWhitespace); + }); + }); + + test("empty and nullish", ({ bench }: TestContext): void => { + bench("empty string", (): void => { + uncapitalize(""); + }); + + bench("null", (): void => { + uncapitalize(null); + }); + + bench("undefined", (): void => { + uncapitalize(undefined); + }); + }); +}); diff --git a/src/libs/uncapitalize/uncapitalize.lib.spec.ts b/src/libs/uncapitalize/uncapitalize.lib.spec.ts new file mode 100644 index 0000000..60f8826 --- /dev/null +++ b/src/libs/uncapitalize/uncapitalize.lib.spec.ts @@ -0,0 +1,103 @@ +import { describe, expect, expectTypeOf, test } from "vitest"; + +import { uncapitalize } from "./uncapitalize.lib"; + +describe("uncapitalize", (): void => { + describe("given a string", (): void => { + test("uncapitalizes an uppercase first character", (): void => { + expect(uncapitalize("Hello")).toBe("hello"); + expect(uncapitalize("Hello world")).toBe("hello world"); + expect(uncapitalize("Typescript")).toBe("typescript"); + }); + + test("preserves an already lowercase first character", (): void => { + expect(uncapitalize("hello")).toBe("hello"); + expect(uncapitalize("hello world")).toBe("hello world"); + expect(uncapitalize("helloWorld")).toBe("helloWorld"); + }); + + test("uncapitalizes only the first character", (): void => { + expect(uncapitalize("HELLO")).toBe("hELLO"); + expect(uncapitalize("HELLO wORLD")).toBe("hELLO wORLD"); + expect(uncapitalize("HELLOworld")).toBe("hELLOworld"); + }); + + test("handles a single-character string", (): void => { + expect(uncapitalize("A")).toBe("a"); + expect(uncapitalize("a")).toBe("a"); + }); + + test("preserves leading whitespace", (): void => { + expect(uncapitalize(" Hello")).toBe(" Hello"); + expect(uncapitalize("\tHello")).toBe("\tHello"); + expect(uncapitalize("\nHello")).toBe("\nHello"); + expect(uncapitalize("\r\nHello")).toBe("\r\nHello"); + expect(uncapitalize("\u00A0Hello")).toBe("\u00A0Hello"); + expect(uncapitalize("\u3000Hello")).toBe("\u3000Hello"); + }); + + test("preserves whitespace-only strings", (): void => { + expect(uncapitalize(" ")).toBe(" "); + expect(uncapitalize("\t")).toBe("\t"); + expect(uncapitalize("\n")).toBe("\n"); + }); + + test("supports Unicode and accented characters", (): void => { + expect(uncapitalize("Éclair")).toBe("éclair"); + expect(uncapitalize("Ñandú")).toBe("ñandú"); + expect(uncapitalize("Über")).toBe("über"); + expect(uncapitalize("Ørebro")).toBe("ørebro"); + expect(uncapitalize("Σigma")).toBe("σigma"); + expect(uncapitalize("สวัสดี")).toBe("สวัสดี"); + expect(uncapitalize("こんにちは")).toBe("こんにちは"); + }); + + test("preserves strings starting with non-letter characters", (): void => { + expect(uncapitalize("123Hello")).toBe("123Hello"); + expect(uncapitalize("!Hello")).toBe("!Hello"); + expect(uncapitalize("@Hello")).toBe("@Hello"); + expect(uncapitalize("-Hello")).toBe("-Hello"); + expect(uncapitalize("_Hello")).toBe("_Hello"); + }); + + test("preserves strings starting with symbols and emojis", (): void => { + expect(uncapitalize("🔥Hello")).toBe("🔥Hello"); + expect(uncapitalize("❤️Hello")).toBe("❤️Hello"); + expect(uncapitalize("©Hello")).toBe("©Hello"); + }); + }); + + describe("given a string literal", (): void => { + test("preserves the transformed literal type", (): void => { + expectTypeOf(uncapitalize("Hello")).toEqualTypeOf<"hello">(); + expectTypeOf(uncapitalize("hello")).toEqualTypeOf<"hello">(); + }); + }); + + describe("given an uncapitalized result", (): void => { + test("is idempotent", (): void => { + const inputs: string[] = ["Hello", "hello", "HELLO", "A", "a", " Hello", "Éclair", "123Hello", "🔥Hello"]; + + for (let index: number = 0; index < inputs.length; index++) { + const input: string = inputs[index]; + const result: string = uncapitalize(input); + + expect(uncapitalize(result)).toBe(result); + } + }); + }); + + describe("given an empty or nullish value", (): void => { + test("returns an empty string for an empty string", (): void => { + expect(uncapitalize("")).toBe(""); + }); + + test("returns an empty string for null", (): void => { + expect(uncapitalize(null)).toBe(""); + }); + + test("returns an empty string for undefined", (): void => { + expect(uncapitalize(undefined)).toBe(""); + }); + }); +}); diff --git a/src/libs/uncapitalize/uncapitalize.lib.ts b/src/libs/uncapitalize/uncapitalize.lib.ts new file mode 100644 index 0000000..3690b89 --- /dev/null +++ b/src/libs/uncapitalize/uncapitalize.lib.ts @@ -0,0 +1,45 @@ +import { EMPTY } from "../../constants"; + +import { hasLength } from "../has-length"; + +/** + * Converts the first character of a string to lowercase while preserving its literal type. + * + * @param value - The string to uncapitalize. + * @returns The uncapitalized string with its literal type preserved. + * + * @example + * + * uncapitalize("Hello"); // "hello" + * uncapitalize("HelloWorld"); // "helloWorld" + * uncapitalize("hello"); // "hello" + */ +export function uncapitalize(value: T): Uncapitalize; + +/** + * Converts the first character of a string to lowercase. + * + * @param value - The string to uncapitalize, or `null` or `undefined`. + * @returns The uncapitalized string, or an empty string when `value` is empty, `null`, or `undefined`. + * + * @example + * + * uncapitalize("Hello"); // "hello" + * uncapitalize("HelloWorld"); // "helloWorld" + * uncapitalize("hello"); // "hello" + * uncapitalize(""); // "" + * uncapitalize(null); // "" + * uncapitalize(undefined); // "" + */ +export function uncapitalize(value: string | null | undefined): string; + +export function uncapitalize(value: string | null | undefined): string { + if (!hasLength(value)) { + return EMPTY; + } + + const firstChar: string = value[0]; + const lowerFirst: string = firstChar.toLowerCase(); + + return firstChar === lowerFirst ? value : lowerFirst + value.slice(1); +} diff --git a/src/types/character-segment-mode.type.ts b/src/types/character-segment-mode.type.ts new file mode 100644 index 0000000..f799053 --- /dev/null +++ b/src/types/character-segment-mode.type.ts @@ -0,0 +1,8 @@ +/** + * Character segmentation mode used to determine character boundaries. + * + * - `"utf16"`: Operates on UTF-16 code units (fastest, default). + * - `"codePoint"`: Operates on Unicode code points (handles single emojis and surrogate pairs). + * - `"grapheme"`: Operates on user-perceived grapheme clusters (handles complex emoji sequences and combining marks). + */ +export type CharacterSegmentMode = "utf16" | "codePoint" | "grapheme"; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..41b14e0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export * from "./character-segment-mode.type"; diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..b960f81 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*"], + "exclude": [ + "**/*.bench.ts", + "**/*.spec.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..53dda27 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "baseUrl": "./", + "isolatedModules": true, + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "module": "esnext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "noEmit": true, + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "es2022", + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": false + } +} diff --git a/tsconfig.types.json b/tsconfig.types.json new file mode 100644 index 0000000..118810c --- /dev/null +++ b/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "./dist/types" + } +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..9ee0de7 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,51 @@ +import { defineConfig, Options } from "tsup"; + +export default defineConfig((): Options[] => { + const base: Options = { + tsconfig: "./tsconfig.build.json", + dts: false, + platform: "neutral", + splitting: false, + sourcemap: true + }; + + return [ + { + entry: ["src/index.ts"], + format: "cjs", + minify: false, + outDir: "./dist/cjs", + outExtension: () => { + return { + js: ".cjs" + }; + }, + ...base + }, + { + entry: ["src/index.ts"], + format: "esm", + minify: false, + outDir: "./dist/esm", + outExtension: () => { + return { + js: ".mjs" + }; + }, + ...base + }, + { + entry: ["src/index.ts"], + format: "iife", + minify: true, + globalName: "stringUtils", + outDir: "./dist/iife", + outExtension: () => { + return { + js: ".global.js" + }; + }, + ...base + } + ]; +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0df2c43 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.spec.ts"], + environment: "node", + benchmark: { + include: ["**/*.bench.ts"] + }, + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: ["**/index.ts", "**/*.spec.ts", "**/*.bench.ts"], + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100 + }, + reportsDirectory: "./coverage" + } + } +});