├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── branch-test-coverage.yml │ ├── inactive_contributions.yml │ ├── publish.yml │ ├── pull_request.yml │ └── reusable_workflow_test.yml ├── .gitignore ├── .yarnrc ├── LICENSE ├── README.md ├── build └── tasks │ ├── generate_palette.js │ └── generate_scheme.js ├── jest.config.ts ├── package.json ├── package.swcrc ├── src ├── animation │ ├── animate.ts │ └── index.ts ├── array │ ├── arrays.test.ts │ ├── arrays.ts │ └── index.ts ├── async │ ├── asyncImportLoader.ts │ ├── debounce.test.ts │ ├── debounce.ts │ ├── index.ts │ ├── throttle.test.ts │ └── throttle.ts ├── datetime │ ├── date.ts │ ├── duration.test.ts │ ├── duration.ts │ ├── index.ts │ ├── isDateToday.test.ts │ ├── isDateToday.ts │ ├── isSameDate.test.ts │ └── isSameDate.ts ├── device │ ├── IOSDetections.test.ts │ ├── IOSDetections.ts │ ├── InputUtils.ts │ ├── index.ts │ └── retina.ts ├── html │ ├── __tests__ │ │ └── htmlEntities.test.ts │ ├── entity.ts │ ├── escape.ts │ └── index.ts ├── index.ts ├── internal │ ├── codepoints.ts │ ├── replacer.ts │ └── uniqueArray.ts ├── other │ ├── clipboard.ts │ ├── common.test.ts │ ├── common.ts │ ├── cookie.ts │ ├── detections.ts │ ├── dom.ts │ ├── equal.ts │ ├── functions.test.ts │ ├── functions.ts │ ├── getOffsetRect.ts │ ├── getPhotoSize.test.ts │ ├── getPhotoSize.ts │ ├── index.ts │ ├── numbers.ts │ ├── objects.ts │ ├── querystring.test.ts │ ├── querystring.ts │ ├── random.ts │ ├── react_utils.test.ts │ ├── react_utils.ts │ ├── regexp.ts │ ├── storage.ts │ └── types.ts ├── text │ ├── index.ts │ ├── numbers.ts │ ├── transliteration.test.ts │ └── transliteration.ts └── typecheck │ ├── index.ts │ ├── type_checkers.test.ts │ └── type_checkers.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | docs/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es6": true, // Enable global es6 variables, like Set, Map, etc 5 | "browser": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "parser": "@typescript-eslint/parser", 10 | "plugins": ["import", "prettier"], 11 | "parserOptions": { 12 | "project": "./tsconfig.json", 13 | "ecmaVersion": 2018, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "jsx": true, 17 | "restParams": true, 18 | "spread": true 19 | } 20 | }, 21 | "extends": [ 22 | "plugin:@vkontakte/eslint-plugin/typescript", // "Preset 1" 23 | "prettier" // "Preset 2" (overrides "Preset 1") 24 | ], 25 | "rules": { 26 | "prettier/prettier": "error", 27 | 28 | "@typescript-eslint/explicit-member-accessibility": "off", // [Reason] overrides "Preset 1" 29 | "@typescript-eslint/no-unnecessary-condition": "off", // [Reason] overrides "Preset 1" 30 | "@typescript-eslint/no-magic-numbers": "off", // [Reason] overrides "Preset 1" 31 | "@typescript-eslint/no-non-null-assertion": "off", // [Reason] overrides "Preset 1" 32 | 33 | "spaced-comment": ["error", "always", { "exceptions": ["#__PURE__"] }] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/ @VKCOM/vk-sec 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | allow: 8 | - dependency-type: 'direct' 9 | groups: 10 | eslint: 11 | patterns: 12 | - 'eslint*' 13 | - '@typescript-eslint/*' 14 | - '@vkontakte/eslint-plugin' 15 | jest: 16 | patterns: 17 | - 'jest*' 18 | - '@jest/*' 19 | - '@swc/jest' 20 | typedoc: 21 | patterns: 22 | - 'typedoc*' 23 | size-limit: 24 | patterns: 25 | - 'size-limit' 26 | - '@size-limit/*' 27 | prettier: 28 | patterns: 29 | - 'prettier' 30 | - '@vkontakte/prettier-config' 31 | 32 | - package-ecosystem: 'github-actions' 33 | # Workflow files stored in the 34 | # default location of `.github/workflows` 35 | directory: '/' 36 | schedule: 37 | interval: 'weekly' 38 | -------------------------------------------------------------------------------- /.github/workflows/branch-test-coverage.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Требования: 3 | # - Тесты должны запускаться при каждом изменении в ветке, чтобы корректно рассчитывалось покрытие тестами при 4 | # последующих изменениях. 5 | 6 | name: Branch test coverage 7 | 8 | run-name: Check ${{ github.ref_name }} branch test coverage 9 | 10 | on: 11 | push: 12 | branches: 13 | - master 14 | 15 | concurrency: 16 | group: branch-test-coverage-${{ github.ref_name }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | name: Call reusable unit tests workflow 22 | uses: ./.github/workflows/reusable_workflow_test.yml 23 | -------------------------------------------------------------------------------- /.github/workflows/inactive_contributions.yml: -------------------------------------------------------------------------------- 1 | name: 'Close inactive issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '0 10 * * *' # every day at 10:00 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v9 14 | with: 15 | days-before-issue-stale: -1 16 | days-before-issue-close: 14 17 | stale-issue-label: 'issue-needs-more-info' 18 | close-issue-message: 'Задача закрыта из-за отсутствия активности в течение последних 14 дней. Если это произошло по ошибке или проблема все ещё актуальна, откройте задачу повторно.' 19 | days-before-pr-stale: 7 20 | days-before-pr-close: 7 21 | stale-pr-label: 'pr-needs-work' 22 | close-pr-message: 'PR закрыт из-за отсутствия активности в течение последних 14 дней. Если это произошло по ошибке или изменения все ещё актуальны, откройте PR повторно.' 23 | exempt-pr-labels: 'no-stale' 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish' 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | type: 7 | description: 'version type:' 8 | required: true 9 | type: choice 10 | default: 'minor' 11 | options: 12 | - patch 13 | - minor 14 | - major 15 | custom_version: 16 | description: 'custom version: x.y.z (without "v")' 17 | required: false 18 | tag: 19 | description: 'tag' 20 | 21 | run-name: Publish ${{ inputs.type }} ${{ inputs.custom_version }} 22 | 23 | jobs: 24 | publish: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Set Git credentials 32 | run: | 33 | git config --local user.email "actions@github.com" 34 | git config --local user.name "GitHub Action" 35 | 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: 20 39 | cache: 'yarn' 40 | always-auth: true 41 | registry-url: 'https://registry.npmjs.org' 42 | 43 | - run: yarn install --frozen-lockfile --ignore-scripts 44 | 45 | - run: yarn lint 46 | - run: yarn test 47 | 48 | - name: Bump by version type 49 | if: ${{ !github.event.inputs.custom_version }} 50 | run: yarn version --${{ github.event.inputs.type }} --no-commit-hooks 51 | 52 | - name: Bump by custom version 53 | if: ${{ github.event.inputs.custom_version }} 54 | run: yarn version --new-version ${{ github.event.inputs.custom_version }} --no-commit-hooks 55 | 56 | - name: Pushing changes 57 | uses: ad-m/github-push-action@master 58 | with: 59 | github_token: ${{ secrets.GITHUB_TOKEN }} 60 | branch: ${{ github.ref }} 61 | 62 | - name: Publushing prerelase 63 | run: yarn publish --non-interactive --tag ${{ github.event.inputs.tag }} 64 | if: ${{ github.event.inputs.tag }} 65 | env: 66 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_PUBLISH_TOKEN }} 67 | 68 | - name: Publushing release 69 | run: yarn publish --non-interactive 70 | if: ${{ !github.event.inputs.tag }} 71 | env: 72 | NODE_AUTH_TOKEN: ${{ secrets.NPMJS_PUBLISH_TOKEN }} 73 | 74 | docs_build: 75 | needs: 76 | - publish 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v4 80 | - uses: actions/setup-node@v4 81 | with: 82 | node-version: 20 83 | cache: 'yarn' 84 | - run: yarn install --frozen-lockfile 85 | 86 | - run: yarn docs 87 | 88 | - name: Publishing doc 89 | uses: JamesIves/github-pages-deploy-action@v4 90 | with: 91 | folder: docs 92 | token: ${{ secrets.GITHUB_TOKEN }} 93 | branch: gh-pages 94 | target-folder: docs 95 | single-commit: true 96 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: 'Pull Request' 2 | 3 | on: ['pull_request'] 4 | 5 | concurrency: 6 | group: pull_request-${{ github.event.pull_request.number }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | test: 11 | name: Call reusable unit tests workflow 12 | uses: ./.github/workflows/reusable_workflow_test.yml 13 | with: 14 | ref: refs/pull/${{ github.event.pull_request.number }}/merge 15 | 16 | lint: 17 | runs-on: ubuntu-latest 18 | name: Run unit tests 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | cache: 'yarn' 25 | - run: yarn install --frozen-lockfile --ignore-scripts 26 | 27 | - name: Run lints 28 | run: yarn run lint 29 | 30 | size: 31 | runs-on: ubuntu-latest 32 | env: 33 | CI_JOB_NUMBER: 1 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: andresz1/size-limit-action@v1 37 | with: 38 | github_token: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/reusable_workflow_test.yml: -------------------------------------------------------------------------------- 1 | name: 'Reusable workflow / Unit tests' 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | ref: 7 | description: 'The branch, tag or SHA to checkout' 8 | default: ${{ github.ref }} 9 | required: false 10 | type: string 11 | 12 | jobs: 13 | test: 14 | permissions: 15 | id-token: write 16 | runs-on: ubuntu-latest 17 | name: Run unit tests 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | ref: ${{ inputs.ref }} 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: 'yarn' 26 | - run: yarn install --frozen-lockfile --ignore-scripts 27 | 28 | - name: Run tests 29 | run: yarn run test 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v5 33 | with: 34 | use_oidc: true 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | node_modules 3 | .idea 4 | .DS_Store 5 | .vscode 6 | docs/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | frozen-lockfile true 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 VK.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @vkontakte/vkjs 2 | 3 | Набор общих функций для ВКонтакте 4 | 5 | ```sh 6 | yarn add @vkontakte/vkjs 7 | ``` 8 | 9 | [Документация](https://vkcom.github.io/vkjs) 10 | -------------------------------------------------------------------------------- /build/tasks/generate_palette.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const palette = require('@vkontakte/appearance/main.valette/palette'); 4 | 5 | /** 6 | * @param {string} color ahex 7 | * @return {string} color цвет в браузерном представлении 8 | */ 9 | function resolveColor(color) { 10 | if (color.indexOf('#') === 0 && color.length === 9) { // ahex 11 | return ahex2rgba(color.replace('#', '')); 12 | } 13 | return color; 14 | } 15 | 16 | /** 17 | * @param {string} ahex цвет в формате ahex: 00ffffff 18 | * @param {number} multiplier 19 | * @return {string} цвет в формате rgba 20 | */ 21 | function ahex2rgba(ahex, multiplier = 1) { 22 | const opacity = parseInt(ahex.slice(0, 2), 16) / 255 * multiplier; 23 | const colorHex = ahex.slice(2); 24 | return opacify(colorHex, opacity); 25 | } 26 | 27 | /** 28 | * @param {string} hex цвет в формате hex: ffffff 29 | * @param {number} opacity прозрачность в диапазоне [0, 1] 30 | * @return {string} цвет в формате rgba 31 | */ 32 | function opacify(hex, opacity) { 33 | return `rgba(${parseInt(hex.slice(0, 2), 16)}, ${parseInt(hex.slice(2, 4), 16)}, ${parseInt(hex.slice(4), 16)}, ${opacity.toFixed(2)})`; 34 | } 35 | 36 | function generatePalette(options) { 37 | let css = '/* stylelint-disable */\n/*\n* Этот файл сгенерирован автоматически. Не надо править его руками.\n*/\n'; 38 | css += ':root {\n'; 39 | 40 | Object.keys(palette).forEach((colorName) => { 41 | css += ` --${colorName}: ${resolveColor(palette[colorName]).toLowerCase()};\n`; 42 | }); 43 | css += '}\n/* stylelint-enable */'; 44 | fs.writeFileSync(path.resolve(__dirname, options.dir, options.file || 'palette.css'), css); 45 | } 46 | 47 | module.exports = generatePalette; 48 | -------------------------------------------------------------------------------- /build/tasks/generate_scheme.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | /** 5 | * @param {object} palette палитра цветов 6 | * @param {Object} clusterData 7 | * @param {string} clusterData.color_identifier 8 | * @param {number} clusterData.alpha_multiplier 9 | * @return {string} color цвет в браузерном представлении 10 | */ 11 | function resolveColor(palette, clusterData) { 12 | const color = palette[clusterData.color_identifier]; 13 | const alphaMultiplier = clusterData.alpha_multiplier ? Number(clusterData.alpha_multiplier) : 1; 14 | 15 | if (!color) { 16 | console.log('Missing color:', clusterData.color_identifier); 17 | return "#000"; 18 | } else { 19 | if (color.indexOf('#') === 0 && color.length === 9) { // ahex 20 | return ahex2rgba(color.replace('#', ''), alphaMultiplier); 21 | } else if (color.indexOf('#') === 0 && clusterData.alpha_multiplier) { 22 | return opacify(color.replace('#', ''), alphaMultiplier); 23 | } 24 | } 25 | return color; 26 | } 27 | 28 | /** 29 | * @param {string} ahex цвет в формате ahex: 00ffffff 30 | * @param {number} multiplier 31 | * @return {string} цвет в формате rgba 32 | */ 33 | function ahex2rgba(ahex, multiplier = 1) { 34 | const opacity = parseInt(ahex.slice(0, 2), 16) / 255 * multiplier; 35 | const colorHex = ahex.slice(2); 36 | return opacify(colorHex, opacity); 37 | } 38 | 39 | /** 40 | * @param {string} hex цвет в формате hex: ffffff 41 | * @param {number} opacity прозрачность в диапазоне [0, 1] 42 | * @return {string} цвет в формате rgba 43 | */ 44 | function opacify(hex, opacity) { 45 | return `rgba(${parseInt(hex.slice(0, 2), 16)}, ${parseInt(hex.slice(2, 4), 16)}, ${parseInt(hex.slice(4), 16)}, ${opacity.toFixed(2)})`; 46 | } 47 | 48 | /** 49 | * @param {object} scheme схема 50 | * @param {object} palette палитра 51 | * @param {object} defaultSchemeId схема по умолчанию 52 | */ 53 | function generateScheme(scheme, palette, defaultSchemeId, targetDir) { 54 | for (const schemeId in scheme) { 55 | const clusters = scheme[schemeId].colors; 56 | let css = '/* stylelint-disable */\n/*\n* Этот файл сгенерирован автоматически. Не надо править его руками.\n*/\n'; 57 | let selector = `body[scheme="${schemeId}"], [scheme="${schemeId}"], .vkui${schemeId}`; 58 | if (schemeId === defaultSchemeId) { 59 | selector = `:root, ${selector}` 60 | } 61 | css += `${selector} {\n`; 62 | Object.keys(clusters).sort((a, b) => a.localeCompare(b)).forEach((clusterId) => { 63 | css += ` --${clusterId}: ${resolveColor(palette, clusters[clusterId]).toLowerCase()};\n`; 64 | }); 65 | css += '}\n/* stylelint-enable */\n'; 66 | fs.writeFileSync(path.resolve(targetDir, `${schemeId}.css`), css); 67 | } 68 | } 69 | 70 | module.exports = generateScheme; 71 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | 3 | const config = { 4 | transform: { 5 | '^.+\\.(t|j)sx?$': '@swc/jest', 6 | }, 7 | testEnvironment: 'jsdom', 8 | roots: [path.join(__dirname, 'src')], 9 | collectCoverage: true, 10 | coverageReporters: ['text', 'cobertura'], 11 | collectCoverageFrom: ['src/*/**/**.{ts,tsx}'], 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vkontakte/vkjs", 3 | "version": "2.0.1", 4 | "description": "VK shared JS libs", 5 | "type": "module", 6 | "module": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./lib/index.d.ts", 11 | "import": "./lib/index.js", 12 | "default": "./lib/index.js" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "directories": { 17 | "lib": "lib" 18 | }, 19 | "files": [ 20 | "lib", 21 | "src", 22 | "build" 23 | ], 24 | "scripts": { 25 | "clear": "shx rm -rf lib", 26 | "prepare": "yarn build", 27 | "build": "yarn clear && concurrently 'yarn:build:*'", 28 | "test": "jest", 29 | "lint": "concurrently 'yarn:lint:*'", 30 | "lint:tsc": "tsc --noEmit", 31 | "lint:eslint": "eslint ./src --ext .ts --fix", 32 | "prepublishOnly": "yarn build", 33 | "publish-package": "yarn install --check-files && yarn publish --non-interactive", 34 | "swc-base": "swc src/ --config-file package.swcrc --strip-leading-paths", 35 | "build:es6": "yarn swc-base -d lib", 36 | "build:types": "cross-env NODE_ENV=production tsc --project tsconfig.build.json", 37 | "size": "yarn build && size-limit", 38 | "docs": "typedoc" 39 | }, 40 | "pre-commit": [ 41 | "test", 42 | "lint" 43 | ], 44 | "size-limit": [ 45 | { 46 | "name": "JS", 47 | "path": "lib/index.js", 48 | "import": "*" 49 | }, 50 | { 51 | "name": "JS ES6 with querystring only import (tree shaking)", 52 | "path": "lib/index.js", 53 | "import": "{ querystring }" 54 | }, 55 | { 56 | "name": "JS ES6 with leadingZero only import (tree shaking)", 57 | "path": "lib/index.js", 58 | "import": "{ leadingZero }" 59 | }, 60 | { 61 | "name": "JS ES6 with decodeHTMLEntities only import (tree shaking)", 62 | "path": "lib/index.js", 63 | "import": "{ decodeHTMLEntities }" 64 | }, 65 | { 66 | "name": "JS ES6 with decodeHTMLFullEntities only import (tree shaking)", 67 | "path": "lib/index.js", 68 | "import": "{ decodeHTMLFullEntities }" 69 | } 70 | ], 71 | "repository": { 72 | "type": "git", 73 | "url": "git+https://github.com/VKCOM/vkjs.git" 74 | }, 75 | "author": "VK Team ", 76 | "license": "MIT", 77 | "bugs": { 78 | "url": "https://github.com/VKCOM/vkjs/issues" 79 | }, 80 | "homepage": "https://github.com/VKCOM/vkjs#readme", 81 | "devDependencies": { 82 | "@jest/globals": "^29.4.2", 83 | "@size-limit/file": "^11.0.2", 84 | "@size-limit/webpack": "^11.0.2", 85 | "@swc/cli": "^0.7.3", 86 | "@swc/core": "^1.3.49", 87 | "@swc/jest": "^0.2.24", 88 | "@types/node": "^22.5.5", 89 | "@types/react": "^19.0.1", 90 | "@typescript-eslint/eslint-plugin": "^7.0.1", 91 | "@typescript-eslint/parser": "^7.0.1", 92 | "@vkontakte/eslint-plugin": "^1.1.1", 93 | "@vkontakte/prettier-config": "^0.2.1", 94 | "concurrently": "^9.0.0", 95 | "cross-env": "^7.0.3", 96 | "eslint": "^8.42.0", 97 | "eslint-config-prettier": "^9.0.0", 98 | "eslint-plugin-import": "^2.27.5", 99 | "eslint-plugin-prettier": "^5.0.0", 100 | "jest": "^29.1.2", 101 | "jest-environment-jsdom": "^29.1.2", 102 | "pre-commit": "1.2.2", 103 | "prettier": "^3.0.2", 104 | "react": "^19.0.0", 105 | "shx": "^0.4.0", 106 | "size-limit": "^11.0.0", 107 | "ts-node": "^10.9.2", 108 | "typedoc": "^0.28.0", 109 | "typedoc-plugin-mdn-links": "^5.0.1", 110 | "typescript": "^5.1.3" 111 | }, 112 | "resolutions": { 113 | "@typescript-eslint/eslint-plugin": "7.4.0", 114 | "@typescript-eslint/parser": "7.4.0" 115 | }, 116 | "dependencies": { 117 | "@swc/helpers": "^0.5.0", 118 | "clsx": "^2.1.1" 119 | }, 120 | "prettier": "@vkontakte/prettier-config", 121 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 122 | } 123 | -------------------------------------------------------------------------------- /package.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "exclude": ["\\.(test|spec|e2e)\\.[jt]sx?$", "testing/"], 4 | "jsc": { 5 | "externalHelpers": true, 6 | "parser": { 7 | "syntax": "typescript" 8 | }, 9 | "target": "es5", 10 | "preserveAllComments": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "*": ["node_modules", "src/*"] 14 | } 15 | }, 16 | "module": { 17 | "type": "es6", 18 | "resolveFully": true 19 | }, 20 | "sourceMaps": true 21 | } 22 | -------------------------------------------------------------------------------- /src/animation/animate.ts: -------------------------------------------------------------------------------- 1 | import { canUseDOM, canUseEventListeners } from '../other/dom'; 2 | import { type SupportEvent } from '../other/types'; 3 | 4 | type TimingInterface = (timeFraction: number) => number; 5 | 6 | type DrawInterface = (progress: number) => void; 7 | 8 | interface AnimateArgumentsInterface { 9 | /** 10 | * Длительность 11 | */ 12 | duration: number; 13 | 14 | /** 15 | * Тайминг функция анимации 16 | */ 17 | timing: TimingInterface; 18 | 19 | /** 20 | * Коллбэк, в который прокидывается прогресс [0, 1] 21 | */ 22 | draw: DrawInterface; 23 | } 24 | 25 | /** 26 | * Функция для js анимации 27 | */ 28 | export function animate({ duration, timing, draw }: AnimateArgumentsInterface) { 29 | if (!canUseDOM) { 30 | return; 31 | } 32 | 33 | const start = window.performance.now(); 34 | 35 | // eslint-disable-next-line no-shadow 36 | window.requestAnimationFrame(function animate(time) { 37 | let timeFraction = (time - start) / duration; 38 | 39 | if (timeFraction > 1) { 40 | timeFraction = 1; 41 | } 42 | 43 | const progress = timing(timeFraction); 44 | 45 | draw(progress); 46 | 47 | if (timeFraction < 1) { 48 | window.requestAnimationFrame(animate); 49 | } 50 | }); 51 | } 52 | 53 | // WebKitAnimationEvent и WebKitTransitionEvent не существуют в глобальном контексте 54 | declare const WebKitAnimationEvent: AnimationEvent; 55 | declare const WebKitTransitionEvent: TransitionEvent; 56 | 57 | export const animationEvent = /*#__PURE__*/ (() => { 58 | const obj: SupportEvent<'animationend'> = { 59 | supported: false, 60 | name: 'animationend', 61 | }; 62 | 63 | if (canUseDOM) { 64 | if (typeof AnimationEvent !== 'undefined') { 65 | obj.supported = true; 66 | } else if (typeof WebKitAnimationEvent !== 'undefined') { 67 | obj.supported = true; 68 | 69 | // webkitAnimationEnd не входит в перечисление событий, но соответствует animationend 70 | obj.name = 'webkitAnimationEnd' as unknown as 'animationend'; 71 | } 72 | } 73 | 74 | return obj; 75 | })(); 76 | 77 | export const transitionEvent = /*#__PURE__*/ (() => { 78 | const obj: SupportEvent<'transitionend'> = { 79 | supported: false, 80 | name: 'transitionend', 81 | }; 82 | 83 | if (canUseDOM) { 84 | if (typeof TransitionEvent !== 'undefined') { 85 | obj.supported = true; 86 | } else if (typeof WebKitTransitionEvent !== 'undefined') { 87 | obj.supported = true; 88 | 89 | // webkitTransitionEnd не входит в перечисление событий, но соответствует transitionend 90 | obj.name = 'webkitTransitionEnd' as unknown as 'transitionend'; 91 | } 92 | } 93 | 94 | return obj; 95 | })(); 96 | 97 | /** 98 | * Ожидание окончания анимации на элементе 99 | * 100 | * @param listener Коллбэк окончания ожидания 101 | * @param fallbackTime Сколько ждать в мс если событие не поддерживается 102 | * @param el Элемент 103 | */ 104 | export function waitAnimationEnd( 105 | listener: (ev?: AnimationEvent) => any, 106 | fallbackTime: number, 107 | el?: GlobalEventHandlers, 108 | ) { 109 | if (canUseEventListeners) { 110 | if (animationEvent.supported && el) { 111 | el.addEventListener(animationEvent.name, listener); 112 | } else { 113 | return window.setTimeout(listener, fallbackTime); 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * Прекращение ожидания окончания анимации на элементе 120 | * 121 | * @param listener Коллбэк окончания ожидания 122 | * @param handle то, что вернулось из ```waitAnimationEnd``` 123 | * @param el Элемент 124 | */ 125 | export function cancelWaitAnimationEnd( 126 | listener: (ev?: AnimationEvent) => any, 127 | handle?: number, 128 | el?: GlobalEventHandlers, 129 | ) { 130 | if (canUseEventListeners) { 131 | if (animationEvent.supported && el) { 132 | el.removeEventListener(animationEvent.name, listener); 133 | } else { 134 | window.clearTimeout(handle); 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Ожидание окончания анимации перехода на элементе 141 | * 142 | * @param listener Коллбэк окончания ожидания 143 | * @param fallbackTime Сколько ждать в мс если событие не поддерживается 144 | * @param el Элемент 145 | */ 146 | export function waitTransitionEnd( 147 | el: GlobalEventHandlers, 148 | listener: (ev?: TransitionEvent) => any, 149 | fallbackTime: number, 150 | ) { 151 | if (canUseEventListeners) { 152 | if (transitionEvent.supported && el) { 153 | el.addEventListener(transitionEvent.name, listener); 154 | } else { 155 | return window.setTimeout(listener, fallbackTime); 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * Прекращение ожидания окончания анимации перехода на элементе 162 | * 163 | * @param listener Коллбэк окончания ожидания 164 | * @param handle То, что вернулось из ```waitTransitionEnd``` 165 | * @param el Элемент 166 | */ 167 | export function cancelWaitTransitionEnd( 168 | listener: (ev?: TransitionEvent) => any, 169 | handle?: number, 170 | el?: GlobalEventHandlers, 171 | ) { 172 | if (canUseEventListeners) { 173 | if (transitionEvent.supported && el) { 174 | el.removeEventListener(transitionEvent.name, listener); 175 | } else { 176 | window.clearTimeout(handle); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/animation/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | animate, 3 | animationEvent, 4 | transitionEvent, 5 | waitAnimationEnd, 6 | cancelWaitAnimationEnd, 7 | waitTransitionEnd, 8 | cancelWaitTransitionEnd, 9 | } from './animate'; 10 | -------------------------------------------------------------------------------- /src/array/arrays.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from '@jest/globals'; 2 | import { createArray, chunkArray, uniqueArray } from './arrays'; 3 | import { uniqueArrayFallback } from '../internal/uniqueArray'; 4 | 5 | test('createArray', () => { 6 | expect(createArray(0)).toEqual([]); 7 | expect(createArray(2)).toEqual([0, 1]); 8 | expect(createArray(4, 2)).toEqual([2, 3, 4, 5]); 9 | }); 10 | 11 | test('chunkArray', () => { 12 | expect(chunkArray(null as any, 1)).toEqual([]); 13 | expect(chunkArray(undefined as any, 1)).toEqual([]); 14 | expect(chunkArray(0 as any, 1)).toEqual([]); 15 | 16 | expect(chunkArray([], 0)).toEqual([]); 17 | expect(chunkArray([1, 2], 0)).toEqual([[1, 2]]); 18 | 19 | expect(chunkArray([], 1)).toEqual([]); 20 | 21 | const array3 = [1, 1, 1]; 22 | 23 | expect(chunkArray(array3, 1)).toEqual([[1], [1], [1]]); 24 | expect(chunkArray(array3, 2)).toEqual([[1, 1], [1]]); 25 | expect(chunkArray(array3, 3)).toEqual([[1, 1, 1]]); 26 | expect(chunkArray(array3, 4)).toEqual([[1, 1, 1]]); 27 | 28 | const array4 = [1, 2, 3, 4]; 29 | 30 | expect(chunkArray(array4, 1)).toEqual([[1], [2], [3], [4]]); 31 | expect(chunkArray(array4, 2)).toEqual([ 32 | [1, 2], 33 | [3, 4], 34 | ]); 35 | expect(chunkArray(array4, 3)).toEqual([[1, 2, 3], [4]]); 36 | expect(chunkArray(array4, 4)).toEqual([array4]); 37 | expect(chunkArray(array4, 5)).toEqual([array4]); 38 | }); 39 | 40 | describe('chunkArray', () => { 41 | const entries = [ 42 | [null as any, []], 43 | [undefined as any, []], 44 | [0 as any, []], 45 | 46 | [ 47 | [1, 1, 1, 2, 2, 2], 48 | [1, 2], 49 | ], 50 | 51 | [ 52 | [1, 2, 2, 1, 1, 3], 53 | [1, 2, 3], 54 | ], 55 | 56 | [ 57 | [1, 1, 2, 3, 5, 5, 7], 58 | [1, 2, 3, 5, 7], 59 | ], 60 | 61 | [ 62 | ['a', 'a', 'b', 'a', 'c', 'a', 'd'], 63 | ['a', 'b', 'c', 'd'], 64 | ], 65 | 66 | [ 67 | [0, '0', 0, 'false', false, false], 68 | [0, '0', 'false', false], 69 | ], 70 | ]; 71 | 72 | test.each(entries)('uniqueArray(%j) should equal %j', (input, expected) => { 73 | expect(uniqueArray(input)).toEqual(expected); 74 | }); 75 | 76 | test.each(entries)('uniqueArray(%j) should equal %j with no Set available', (input, expected) => { 77 | expect(uniqueArrayFallback(input)).toEqual(expected); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/array/arrays.ts: -------------------------------------------------------------------------------- 1 | import { uniqueArrayFallback } from '../internal/uniqueArray'; 2 | 3 | /** 4 | * Создаёт массив чисел требуемой длины 5 | * 6 | * @example 7 | * ```ts 8 | * import assert from 'node:assert'; 9 | * import { createArray } from '@vkontakte/vkjs'; 10 | * 11 | * assert.deepStrictEqual(createArray(5), [0, 1, 2, 3, 4]); 12 | * assert.deepStrictEqual(createArray(3, 2), [2, 3, 4]); 13 | * ``` 14 | * 15 | * @param arrayLength Длина массива 16 | * @param startIndex Начальный индекс (по умолчанию 0) 17 | */ 18 | export function createArray(arrayLength: number, startIndex = 0): number[] { 19 | return Array.from({ length: arrayLength }, (_, index) => startIndex + index); 20 | } 21 | 22 | /** 23 | * Вычисляет сумму элементов массива 24 | * 25 | * @example 26 | * ```ts 27 | * import assert from 'node:assert'; 28 | * import { createArray } from '@vkontakte/vkjs'; 29 | * 30 | * assert.strictEqual(sumArray([0, 1, 2, 3, 4]), 10); 31 | * ``` 32 | */ 33 | export function sumArray(array: readonly number[]): number { 34 | if (!Array.isArray(array) || !array.length) { 35 | return 0; 36 | } 37 | return array.reduce((previous, current) => current + previous); 38 | } 39 | 40 | /** 41 | * Находит среднее арифметическое элементов массива 42 | * 43 | * @example 44 | * ```ts 45 | * import assert from 'node:assert'; 46 | * import { createArray } from '@vkontakte/vkjs'; 47 | * 48 | * assert.strictEqual(averageArray([0, 1, 2, 3, 4]), 2); 49 | * ``` 50 | */ 51 | export function averageArray(array: readonly number[]): number { 52 | if (!Array.isArray(array) || !array.length) { 53 | return 0; 54 | } 55 | return sumArray(array) / array.length; 56 | } 57 | 58 | /** 59 | * Возвращает новый массив с уникальными элементами 60 | * 61 | * @example 62 | * ```ts 63 | * import assert from 'node:assert'; 64 | * import { createArray } from '@vkontakte/vkjs'; 65 | * 66 | * assert.deepStrictEqual(uniqueArray([1, 1, 2, 2, 3]), [1, 2, 3]); 67 | * ``` 68 | */ 69 | export function uniqueArray(array: readonly T[]): T[] { 70 | if (!Array.isArray(array) || !array.length) { 71 | return []; 72 | } 73 | 74 | if (typeof Set !== 'undefined') { 75 | return Array.from(new Set(array)); 76 | } 77 | 78 | return uniqueArrayFallback(array); 79 | } 80 | 81 | /** 82 | * Возвращает новый перемешанный массив 83 | */ 84 | export function shuffleArray(array: readonly T[]): T[] { 85 | const result = array.slice(); 86 | 87 | for (let i = result.length - 1; i > 0; i--) { 88 | const j = Math.floor(Math.random() * (i + 1)); 89 | 90 | [result[i], result[j]] = [result[j], result[i]]; 91 | } 92 | 93 | return result; 94 | } 95 | 96 | /** 97 | * Разбивает массив на чанки 98 | * 99 | * @example 100 | * ```ts 101 | * import assert from 'node:assert'; 102 | * import { createArray } from '@vkontakte/vkjs'; 103 | * 104 | * assert.deepStrictEqual( 105 | * chunkArray([1,2,3,4,5,6,7], 2), 106 | * [[1,2], [3,4], [5,6], [7]], 107 | * ); 108 | * ``` 109 | */ 110 | export function chunkArray(array: readonly T[], size: number): T[][] { 111 | if (!Array.isArray(array) || !array.length) { 112 | return []; 113 | } 114 | 115 | if (!size) { 116 | return [array]; 117 | } 118 | 119 | const head = array.slice(0, size); 120 | const tail = array.slice(size); 121 | 122 | return [head, ...chunkArray(tail, size)]; 123 | } 124 | 125 | /** 126 | * Удаляет из массива элемент по значению. 127 | * Если элемент был удалён – возвращает новый массив. 128 | * 129 | * @example 130 | * 131 | * omitFromArray([1, 2, 3], 3) // [1, 2] 132 | * omitFromArray([1, 2, 3], 5) // [1, 2, 3] 133 | */ 134 | export function omitFromArray(array: T[] = [], value: T): T[] { 135 | const index = array.indexOf(value); 136 | 137 | if (index < 0) { 138 | return array; 139 | } else { 140 | return [...array.slice(0, index), ...array.slice(index + 1)]; 141 | } 142 | } 143 | 144 | /** 145 | * Возвращает разницу между двумя массивами. 146 | * Вернёт элементы, которых не хватает во втором массиве. 147 | * 148 | * @example 149 | * 150 | * difference([1, 2, 3], [1, 2, 3]) // [] 151 | * difference([1, 2, 3], [1]) // [2, 3] 152 | * difference([1, 2, 3], [1, 10, 100]) // [2, 3] 153 | */ 154 | export function difference(array1: readonly T[] = [], array2: readonly T[] = []) { 155 | return array1.reduce((res, item) => { 156 | if (!array2.includes(item)) { 157 | res.push(item); 158 | } 159 | return res; 160 | }, []); 161 | } 162 | -------------------------------------------------------------------------------- /src/array/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createArray, 3 | sumArray, 4 | averageArray, 5 | uniqueArray, 6 | shuffleArray, 7 | chunkArray, 8 | omitFromArray, 9 | difference, 10 | } from './arrays'; 11 | -------------------------------------------------------------------------------- /src/async/asyncImportLoader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Выполняет Promise функцию, пока она не завершится удачей. Может 3 | * использоваться для асинхронной загрузки модулей при плохом интернете 4 | * 5 | * @example 6 | * const HeaderLazyComponent = React.lazy(() => asyncImportLoader(() => import('../components/Header/Header'))); 7 | * 8 | * @example 9 | * asyncImportLoader(() => import('some-module'), 20).then((someModule) => { 10 | * someModule.init(); 11 | * }); 12 | * 13 | * @param asyncImport Функция, которую требуется выполнить 14 | * @param attempts Максимальное количество попыток 15 | */ 16 | export const asyncImportLoader = (asyncImport: () => Promise, attempts = 10): Promise => { 17 | return new Promise((resolve, reject) => { 18 | asyncImport() 19 | .then(resolve) 20 | .catch((error) => { 21 | setTimeout(() => { 22 | if (attempts === 0) { 23 | reject(error); 24 | return; 25 | } 26 | asyncImportLoader(asyncImport, attempts - 1).then(resolve, reject); 27 | }, 1000); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/async/debounce.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, jest, test, describe, beforeEach, it } from '@jest/globals'; 2 | import { debounce } from './debounce'; 3 | import { AnyFunction } from '../other/types'; 4 | 5 | import Mock = jest.Mock; 6 | 7 | describe('debounce', () => { 8 | const delay = 50; 9 | let fn: Mock; 10 | let fnDebounced: ReturnType; 11 | 12 | beforeEach(() => { 13 | jest.useFakeTimers(); 14 | jest.setSystemTime(100); 15 | fn = jest.fn(); 16 | fnDebounced = debounce(fn, delay); 17 | }); 18 | 19 | test('should debounce function call', () => { 20 | fnDebounced(1); 21 | expect(fn.mock.calls).toEqual([]); 22 | 23 | jest.advanceTimersByTime(10); // 10ms 24 | fnDebounced(2); 25 | 26 | jest.advanceTimersByTime(delay - 10); // 50ms 27 | expect(fn.mock.calls).toEqual([]); 28 | 29 | jest.advanceTimersByTime(10); // 60ms 30 | expect(fn.mock.calls).toEqual([[2]]); 31 | }); 32 | 33 | it('should cancel debounced call', function () { 34 | fnDebounced(1); 35 | fnDebounced(2); 36 | fnDebounced(3); 37 | fnDebounced.cancel(); 38 | jest.advanceTimersByTime(delay); 39 | 40 | expect(fn).not.toHaveBeenCalled(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/async/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Функция debounced, которая будет задержана на заданное `delay` время 3 | * в миллисекундах. Если метод будет вызван снова до истечения тайм-аута, 4 | * предыдущий вызов будет прерван. 5 | */ 6 | export interface DebouncedFunction { 7 | (...a: T): void; 8 | 9 | /** 10 | * Отменяет вызов функции 11 | */ 12 | cancel(): void; 13 | } 14 | 15 | /** 16 | * Возвращает debounced функцию, которая задерживает вызов `fn` на заданное 17 | * `delay` время в миллисекундах. Если метод вызывается снова до истечения 18 | * тайм-аута, предыдущий вызов будет прерван. 19 | * 20 | * @param fn Функция которую надо "отложить" 21 | * @param delay Время задержки вызова в миллисекундах 22 | * @param context Контекст с которым будет совершен вызов функции 23 | */ 24 | export function debounce( 25 | fn: (...args: T) => unknown, 26 | delay: number, 27 | context = typeof window !== 'undefined' ? window : undefined, 28 | ): DebouncedFunction { 29 | let timeoutId: ReturnType; 30 | let args: T; 31 | 32 | const later = () => fn.apply(context, args); 33 | const debouncedFn = (...a: T) => { 34 | args = a; 35 | clearTimeout(timeoutId); 36 | timeoutId = setTimeout(later, delay); 37 | }; 38 | 39 | debouncedFn.cancel = () => { 40 | clearTimeout(timeoutId); 41 | }; 42 | 43 | return debouncedFn; 44 | } 45 | -------------------------------------------------------------------------------- /src/async/index.ts: -------------------------------------------------------------------------------- 1 | export { asyncImportLoader } from './asyncImportLoader'; 2 | export { debounce, type DebouncedFunction } from './debounce'; 3 | export { throttle } from './throttle'; 4 | -------------------------------------------------------------------------------- /src/async/throttle.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, jest, test, describe, beforeEach, it } from '@jest/globals'; 2 | import { throttle } from './throttle'; 3 | import { AnyFunction } from '../other/types'; 4 | 5 | import Mock = jest.Mock; 6 | 7 | describe('throttle', () => { 8 | const threshold = 50; 9 | let fn: Mock; 10 | let fnThrottled: ReturnType; 11 | 12 | beforeEach(() => { 13 | jest.useFakeTimers(); 14 | jest.setSystemTime(100); 15 | fn = jest.fn(); 16 | fnThrottled = throttle(fn, threshold); 17 | }); 18 | 19 | test('should call functions as usual if they exceed threshold interval', () => { 20 | fnThrottled(1); 21 | expect(fn.mock.calls).toEqual([[1]]); 22 | 23 | jest.advanceTimersByTime(threshold); 24 | fnThrottled(2); 25 | expect(fn.mock.calls).toEqual([[1], [2]]); 26 | }); 27 | 28 | test('should trigger last call at the end of threshold', () => { 29 | fnThrottled(1); 30 | expect(fn.mock.calls).toEqual([[1]]); 31 | jest.advanceTimersByTime(10); 32 | 33 | fnThrottled(2); 34 | jest.advanceTimersByTime(threshold - 10); 35 | expect(fn.mock.calls).toEqual([[1], [2]]); 36 | }); 37 | 38 | test('should call not more than once per threshold and preserve correct call order', () => { 39 | fnThrottled(1); 40 | // call function immediately after the first call 41 | expect(fn.mock.calls).toEqual([[1]]); 42 | 43 | fnThrottled(2); 44 | fnThrottled(3); 45 | jest.advanceTimersByTime(threshold - 10); // 40ms 46 | // throttle following calls until the threshold is reached 47 | expect(fn.mock.calls).toEqual([[1]]); 48 | 49 | fnThrottled(4); 50 | jest.advanceTimersByTime(10); // 50ms 51 | // call function with last arguments after the threshold is reached 52 | expect(fn.mock.calls).toEqual([[1], [4]]); 53 | 54 | jest.advanceTimersByTime(10); // 60ms 55 | fnThrottled(5); 56 | // don't call function immediately and wait until threshold reached 57 | expect(fn.mock.calls).toEqual([[1], [4]]); 58 | 59 | jest.advanceTimersByTime(threshold - 10); // 100ms 60 | expect(fn.mock.calls).toEqual([[1], [4], [5]]); 61 | }); 62 | 63 | it('should cancel throttled call', function () { 64 | fnThrottled(1); 65 | fnThrottled(2); 66 | fnThrottled(3); 67 | fnThrottled.cancel(); 68 | jest.advanceTimersByTime(threshold); 69 | 70 | expect(fn).toHaveBeenCalledTimes(1); 71 | expect(fn).toHaveBeenCalledWith(1); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/async/throttle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Возвращает throttled функцию, которая задерживает вызов `fn` на 3 | * `threshold` миллисекунд от последнего вызова. Если метод вызывается снова до 4 | * выполнения предыдущего, предыдущий вызов будет прерван. 5 | * 6 | * @param fn Функция, которую надо вызывать 7 | * @param threshold Длительность в миллисекундах 8 | * @param scope Контекст, с которым будет совершен вызов функции 9 | */ 10 | export function throttle( 11 | fn: (...args: T) => unknown, 12 | threshold = 50, 13 | scope = typeof window !== 'undefined' ? window : undefined, 14 | ) { 15 | let prevDate: number = Date.now() - threshold; 16 | let timeoutId: ReturnType; 17 | 18 | const throttledFn = (...args: T) => { 19 | const timeLeft = prevDate + threshold - Date.now(); 20 | 21 | clearTimeout(timeoutId); 22 | if (timeLeft > 0) { 23 | timeoutId = setTimeout(() => { 24 | prevDate = Date.now(); 25 | fn.apply(scope, args); 26 | }, timeLeft); 27 | return; 28 | } 29 | 30 | prevDate = Date.now(); 31 | fn.apply(scope, args); 32 | }; 33 | 34 | throttledFn.cancel = () => { 35 | clearTimeout(timeoutId); 36 | }; 37 | 38 | return throttledFn; 39 | } 40 | -------------------------------------------------------------------------------- /src/datetime/date.ts: -------------------------------------------------------------------------------- 1 | import { leadingZero } from '../other/numbers'; 2 | import { isDateToday } from './isDateToday'; 3 | 4 | export const SECONDS_IN_THE_DAY = 86400; 5 | const MILLISECONDS_IN_THE_DAY = SECONDS_IN_THE_DAY * 1000; 6 | 7 | /** 8 | * Проверяет, что переданная дата - вчерашний день 9 | * 10 | * @example 11 | * ```ts 12 | * import assert from 'node:assert'; 13 | * import { isDateYesterday } from '@vkontakte/vkjs'; 14 | * 15 | * assert.strictEqual(isDateYesterday(new Date(), false); 16 | * ``` 17 | */ 18 | export function isDateYesterday(date: Date): boolean { 19 | const yesterdayDate = new Date(date.getTime() + MILLISECONDS_IN_THE_DAY); 20 | return isDateToday(yesterdayDate); 21 | } 22 | 23 | /** 24 | * Проверяет, что переданная дата - завтрашний день 25 | * 26 | * @example 27 | * ```ts 28 | * import assert from 'node:assert'; 29 | * import { isDateTomorrow } from '@vkontakte/vkjs'; 30 | * 31 | * assert.strictEqual(isDateTomorrow(new Date(), false); 32 | * ``` 33 | */ 34 | export function isDateTomorrow(date: Date): boolean { 35 | const tomorrowDate = new Date(date.getTime() - MILLISECONDS_IN_THE_DAY); 36 | return isDateToday(tomorrowDate); 37 | } 38 | 39 | /** 40 | * Возвращает новую дату — начало переданного дня 41 | * 42 | * @example 43 | * ```ts 44 | * import assert from 'node:assert'; 45 | * import { getBeginningOfDay } from '@vkontakte/vkjs'; 46 | * 47 | * assert.deepStrictEqual( 48 | * getBeginningOfDay(new Date(2024, 0, 1, 12, 34, 56, 789)), 49 | * new Date(2024, 0, 1), 50 | * ); 51 | * ``` 52 | * 53 | * @param date Дата 54 | */ 55 | export function getBeginningOfDay(date: Date) { 56 | const year = date.getFullYear(); 57 | const month = date.getMonth(); 58 | const day = date.getDate(); 59 | 60 | return new Date(year, month, day, 0, 0, 0, 0); 61 | } 62 | 63 | /** 64 | * Возвращает true, если год високосный 65 | * 66 | * @example 67 | * ```ts 68 | * import assert from 'node:assert'; 69 | * import { isLeapYear } from '@vkontakte/vkjs'; 70 | * 71 | * assert.strictEqual(isLeapYear(2024), true); 72 | * assert.strictEqual(isLeapYear(2025), false); 73 | * ``` 74 | * 75 | * @param year Год 76 | */ 77 | export function isLeapYear(year: number) { 78 | return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; 79 | } 80 | 81 | /** 82 | * Возвращает кол-во дней в месяце (последнее число месяца) 83 | * 84 | * @example 85 | * ```ts 86 | * import assert from 'node:assert'; 87 | * import { getLastDayOfMonth } from '@vkontakte/vkjs'; 88 | * 89 | * assert.strictEqual(getLastDayOfMonth(2024, 2), 29); 90 | * assert.strictEqual(getLastDayOfMonth(2025, 2), 28); 91 | * ``` 92 | * 93 | * @param year Год 94 | * @param month Месяц 95 | */ 96 | export function getLastDayOfMonth(year: number, month: number): number { 97 | if (+month === 2) { 98 | return isLeapYear(year) ? 29 : 28; 99 | } else if (month > 0 && ((month < 8 && month % 2 === 0) || (month > 7 && month % 2 === 1))) { 100 | return 30; 101 | } 102 | return 31; 103 | } 104 | 105 | /** 106 | * Ближайший понедельник в прошлом относительно date 107 | * 108 | * @example 109 | * ```ts 110 | * import assert from 'node:assert'; 111 | * import { getStartOfWeek } from '@vkontakte/vkjs'; 112 | * 113 | * assert.deepStrictEqual( 114 | * getStartOfWeek(new Date(2024, 0, 1), 10), 115 | * new Date(2024, 11, 31), 116 | * ); 117 | * ``` 118 | * 119 | * @param date Дата 120 | */ 121 | export function getStartOfWeek(date: Date): Date { 122 | const weekDay = date.getDay(); 123 | if (weekDay === 0) { 124 | return addDays(date, -6); 125 | } 126 | return addDays(date, -weekDay + 1); 127 | } 128 | 129 | /** 130 | * Добавляет дни к дате и возвращает новый объект 131 | * 132 | * @example 133 | * ```ts 134 | * import assert from 'node:assert'; 135 | * import { addDays } from '@vkontakte/vkjs'; 136 | * 137 | * assert.deepStrictEqual( 138 | * addDays(new Date(2024, 0, 1), 10), 139 | * new Date(2024, 0, 11), 140 | * ); 141 | * ``` 142 | * 143 | * @param date Дата 144 | * @param dayCount Количество дней, которые требуется добавить 145 | */ 146 | export function addDays(date: Date, dayCount: number): Date { 147 | const modified = new Date(date.getTime()); 148 | modified.setDate(modified.getDate() + dayCount); 149 | return modified; 150 | } 151 | 152 | /** 153 | * Создаёт дату из Unix Timestamp 154 | * 155 | * @example 156 | * ```ts 157 | * import assert from 'node:assert'; 158 | * import { createDateFromUnixTimestamp } from '@vkontakte/vkjs'; 159 | * 160 | * assert.deepStrictEqual( 161 | * createDateFromUnixTimestamp(1704056400), 162 | * new Date(2024, 0, 1), 163 | * ); 164 | * ``` 165 | * 166 | * @param timestamp Дата в формате unix timestamp (секунды) 167 | */ 168 | export function createDateFromUnixTimestamp(timestamp: number): Date { 169 | return new Date(timestamp * 1000); 170 | } 171 | 172 | /** 173 | * Возвращает Unix Timestamp из даты 174 | * 175 | * @example 176 | * ```ts 177 | * import assert from 'node:assert'; 178 | * import { getUnixTimestampFromDate } from '@vkontakte/vkjs'; 179 | * 180 | * assert.strictEqual( 181 | * getUnixTimestampFromDate(new Date(2024, 0, 1)), 182 | * 1704056400, 183 | * ); 184 | * ``` 185 | * 186 | * @param date Дата, которую требуется перевести в Unix Timestamp 187 | */ 188 | export function getUnixTimestampFromDate(date: Date): number { 189 | return Math.floor(date.getTime() / 1000); 190 | } 191 | 192 | /** 193 | * Возвращает дату в формате YYYY-MM-DD 194 | * 195 | * @example 196 | * ```ts 197 | * import assert from 'node:assert'; 198 | * import { convertDateToInputFormat } from '@vkontakte/vkjs'; 199 | * 200 | * assert.strictEqual( 201 | * convertDateToInputFormat(new Date(2024, 0, 1)), 202 | * "2024-01-01", 203 | * ); 204 | * ``` 205 | * 206 | * @param date Дата, которую требуется отформатировать 207 | */ 208 | export function convertDateToInputFormat(date: Date): string { 209 | const day = date.getDate(); 210 | const month = date.getMonth() + 1; 211 | const year = date.getFullYear(); 212 | 213 | return [year, leadingZero(month), leadingZero(day)].join('-'); 214 | } 215 | -------------------------------------------------------------------------------- /src/datetime/duration.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { formatDuration } from './duration'; 3 | 4 | test('formatDuration function', () => { 5 | expect(formatDuration(0)).toEqual('0:00'); 6 | expect(formatDuration(1)).toEqual('0:01'); 7 | expect(formatDuration(100)).toEqual('1:40'); 8 | expect(formatDuration(7200)).toEqual('2:00:00'); 9 | expect(formatDuration(14200)).toEqual('3:56:40'); 10 | expect(formatDuration(40200)).toEqual('11:10:00'); 11 | 12 | expect(formatDuration(0, true)).toEqual('0:00:00'); 13 | expect(formatDuration(1, true)).toEqual('0:00:01'); 14 | expect(formatDuration(100, true)).toEqual('0:01:40'); 15 | expect(formatDuration(7200, true)).toEqual('2:00:00'); 16 | expect(formatDuration(14200, true)).toEqual('3:56:40'); 17 | expect(formatDuration(40200, true)).toEqual('11:10:00'); 18 | }); 19 | -------------------------------------------------------------------------------- /src/datetime/duration.ts: -------------------------------------------------------------------------------- 1 | import { leadingZero } from '../other/numbers'; 2 | 3 | /** 4 | * Форматирует длительность в секундах в строку вида "MM:SS" или "HH:MM:SS". 5 | * Если `forceHours` `true`, то всегда будет выводиться "HH:MM:SS", даже если 6 | * длительность меньше часа 7 | * 8 | * @example 9 | * ```ts 10 | * import assert from 'node:assert'; 11 | * import { formatDuration } from '@vkontakte/vkjs'; 12 | * 13 | * assert.strictEqual( 14 | * formatDuration(123456), 15 | * "34:17:36", 16 | * ); 17 | * 18 | * assert.strictEqual( 19 | * formatDuration(1234, true), 20 | * "0:20:34", 21 | * ); 22 | * ``` 23 | * 24 | * @param durationInSeconds Количество секунд, которые требуется отформатировать 25 | * @param forceHours Если `true`, то всегда будет выводиться "HH:MM:SS" даже 26 | * если длительность меньше часа 27 | */ 28 | export function formatDuration(durationInSeconds: number, forceHours?: boolean): string { 29 | if (!durationInSeconds) { 30 | durationInSeconds = 0; 31 | } 32 | 33 | durationInSeconds = Math.abs(durationInSeconds); 34 | 35 | const MINUTE = 60; 36 | const HOUR = 3600; 37 | 38 | const hours = Math.floor(durationInSeconds / HOUR); 39 | const minutes = Math.floor(durationInSeconds / MINUTE) % MINUTE; 40 | const seconds = durationInSeconds % MINUTE; 41 | 42 | if (durationInSeconds >= HOUR || forceHours) { 43 | return [hours, leadingZero(minutes), leadingZero(seconds)].join(':'); 44 | } else { 45 | return [minutes, leadingZero(seconds)].join(':'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/datetime/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | SECONDS_IN_THE_DAY, 3 | isDateYesterday, 4 | isDateTomorrow, 5 | getBeginningOfDay, 6 | isLeapYear, 7 | getLastDayOfMonth, 8 | getStartOfWeek, 9 | addDays, 10 | createDateFromUnixTimestamp, 11 | getUnixTimestampFromDate, 12 | convertDateToInputFormat, 13 | } from '../datetime/date'; 14 | 15 | export { isDateToday } from './isDateToday'; 16 | export { isSameDate } from './isSameDate'; 17 | 18 | export { formatDuration } from './duration'; 19 | -------------------------------------------------------------------------------- /src/datetime/isDateToday.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { isDateToday } from './isDateToday'; 3 | 4 | test('isDateToday to be truthy', () => { 5 | expect(isDateToday(new Date())).toBeTruthy(); 6 | }); 7 | 8 | test('isDateToday to be falsy', () => { 9 | expect(isDateToday(new Date(2024, 0, 1))).toBeFalsy(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/datetime/isDateToday.ts: -------------------------------------------------------------------------------- 1 | import { isSameDate } from './isSameDate'; 2 | 3 | /** 4 | * Проверяет, что переданная дата является сегодняшним днём 5 | * 6 | * @example 7 | * ```ts 8 | * import assert from 'node:assert'; 9 | * import { isDateToday } from '@vkontakte/vkjs'; 10 | * 11 | * assert.ok(isDateToday(new Date()); 12 | * ``` 13 | */ 14 | export function isDateToday(date: Date): boolean { 15 | return isSameDate(date, new Date()); 16 | } 17 | -------------------------------------------------------------------------------- /src/datetime/isSameDate.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { isSameDate } from './isSameDate'; 3 | 4 | test('isSameDate to be truthy', () => { 5 | expect(isSameDate(new Date(2024, 0, 1), new Date(2024, 0, 1))).toBeTruthy(); 6 | expect( 7 | isSameDate(new Date(2024, 0, 1, 12, 34, 56, 789), new Date(2024, 0, 1, 21, 43, 56, 987)), 8 | ).toBeTruthy(); 9 | }); 10 | 11 | test('isSameDate to be falsy', () => { 12 | expect(isSameDate(new Date(2024, 0, 1), new Date(2024, 0, 2))).toBeFalsy(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/datetime/isSameDate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Проверяет, что переданные даты относятся к одному и тому же дню 3 | * 4 | * @example 5 | * ```ts 6 | * import assert from 'node:assert'; 7 | * import { isSameDate } from '@vkontakte/vkjs'; 8 | * 9 | * const d1 = new Date(); 10 | * const d2 = new Date(); 11 | * assert.ok(isSameDate(d1, d2)); 12 | * ``` 13 | */ 14 | export function isSameDate(d1: Date, d2: Date): boolean { 15 | return ( 16 | d1.getDate() === d2.getDate() && 17 | d1.getMonth() === d2.getMonth() && 18 | d1.getFullYear() === d2.getFullYear() 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/device/IOSDetections.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { detectIOS, checkIPadOS } from './IOSDetections'; 3 | import { noop } from '../other/functions'; 4 | 5 | describe(detectIOS, () => { 6 | test.each<[Parameters[0], ReturnType]>([ 7 | [ 8 | undefined, 9 | { 10 | isIPad: false, 11 | isIPhone: false, 12 | isIOS: false, 13 | isIPadOS: false, 14 | iosMajor: 0, 15 | iosMinor: 0, 16 | isWKWebView: false, 17 | isScrollBasedViewport: true, 18 | isIPhoneX: false, 19 | isIOSChrome: false, 20 | }, 21 | ], 22 | [ 23 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15', 24 | { 25 | isIPad: false, 26 | isIPhone: false, 27 | isIOS: false, 28 | isIPadOS: false, 29 | iosMajor: 0, 30 | iosMinor: 0, 31 | isWKWebView: false, 32 | isScrollBasedViewport: true, 33 | isIPhoneX: false, 34 | isIOSChrome: false, 35 | }, 36 | ], 37 | // IPhone 38 | [ 39 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Mobile/15E148 Safari/604.1', 40 | { 41 | isIPad: false, 42 | isIPhone: true, 43 | isIOS: true, 44 | isIPadOS: false, 45 | iosMajor: 16, 46 | iosMinor: 4, 47 | isWKWebView: false, 48 | isScrollBasedViewport: false, 49 | isIPhoneX: false, 50 | isIOSChrome: false, 51 | }, 52 | ], 53 | [ 54 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/112.0.5615.46 Mobile/15E148 Safari/604.1', 55 | { 56 | isIPad: false, 57 | isIPhone: true, 58 | isIOS: true, 59 | isIPadOS: false, 60 | iosMajor: 16, 61 | iosMinor: 4, 62 | isWKWebView: false, 63 | isScrollBasedViewport: false, 64 | isIPhoneX: false, 65 | isIOSChrome: true, 66 | }, 67 | ], 68 | [ 69 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/111.0 Mobile/15E148 Safari/605.1.15', 70 | { 71 | isIPad: false, 72 | isIPhone: true, 73 | isIOS: true, 74 | isIPadOS: false, 75 | iosMajor: 16, 76 | iosMinor: 4, 77 | isWKWebView: false, 78 | isScrollBasedViewport: false, 79 | isIPhoneX: false, 80 | isIOSChrome: false, 81 | }, 82 | ], 83 | // IPad old 84 | [ 85 | 'Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10', 86 | { 87 | isIPad: true, 88 | isIPhone: false, 89 | isIOS: true, 90 | isIPadOS: false, 91 | iosMajor: 3, 92 | iosMinor: 2, 93 | isWKWebView: false, 94 | isScrollBasedViewport: true, 95 | isIPhoneX: false, 96 | isIOSChrome: false, 97 | }, 98 | ], 99 | ])('detectIOS(%s)', (ua, expected) => { 100 | expect(detectIOS(ua)).toStrictEqual(expected); 101 | }); 102 | }); 103 | 104 | describe(checkIPadOS, () => { 105 | test('should return false for Mac OS', () => { 106 | const macOSUserAgent = 107 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'; 108 | 109 | expect(checkIPadOS(macOSUserAgent)).toBeFalsy(); 110 | expect(checkIPadOS(macOSUserAgent.toLowerCase())).toBeFalsy(); 111 | }); 112 | 113 | test('should return true for iPadOS', () => { 114 | const iPadOSUserAgent = 115 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15'; 116 | 117 | Object.defineProperties(document, { 118 | ontouchend: { 119 | get: noop, 120 | }, 121 | }); 122 | 123 | expect(checkIPadOS(iPadOSUserAgent)).toBeTruthy(); 124 | expect(checkIPadOS(iPadOSUserAgent.toLowerCase())).toBeTruthy(); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/device/IOSDetections.ts: -------------------------------------------------------------------------------- 1 | import { canUseDOM } from '../other/dom'; 2 | 3 | export const IPHONE_SAFARI_BOTTOM_BAR = 45; 4 | export const IPHONE_X_SAFARI_BOTTOM_BAR = 85; 5 | 6 | export const IPHONE_KEYBOARD_REJECT_OFFSET = 180; 7 | 8 | // 44 iPhone, 55 iPad, iPad Pro 69 9 | export const IOS_NO_KEYBOARD_ALLOWED_OFFSET = 70; 10 | 11 | export function detectIOS(ua?: string) { 12 | if (!ua) { 13 | ua = canUseDOM ? navigator.userAgent : ''; 14 | } 15 | ua = ua.toLowerCase(); 16 | 17 | const isIPadOS = checkIPadOS(ua); 18 | const isIPad = isIPadOS || ua.includes('ipad'); 19 | const isIPhone = !isIPad && ua.search(/iphone|ipod/) !== -1; 20 | const isIOS = isIPhone || isIPad; 21 | 22 | let iosVersion: string[] | typeof isIOS | ReturnType = 23 | isIOS && ua.match(/os ([\d_]+) like mac os x/i); 24 | let iosMajor = 0; 25 | let iosMinor = 0; 26 | 27 | if (isIPadOS) { 28 | iosMajor = 13; 29 | iosMinor = 0; 30 | } else if (iosVersion) { 31 | iosVersion = iosVersion[1].split('_'); 32 | iosMajor = +iosVersion[0]; 33 | iosMinor = +iosVersion[1]; 34 | } 35 | 36 | iosVersion = null; 37 | 38 | const isScrollBasedViewport = iosMajor < 13 && !(iosMajor === 11 && iosMinor < 3); 39 | const isWKWebView = isIOS && checkWKWebView(ua); 40 | 41 | let isIPhoneX = false; 42 | 43 | if (canUseDOM) { 44 | isIPhoneX = 45 | isIOS && screen.width === 375 && screen.height === 812 && window.devicePixelRatio === 3; 46 | } 47 | 48 | const isIOSChrome = ua.search(/crios/i) !== -1; 49 | 50 | return { 51 | isIPad, 52 | isIPhone, 53 | isIOS, 54 | isIPadOS, 55 | iosMajor, 56 | iosMinor, 57 | isWKWebView, 58 | isScrollBasedViewport, 59 | isIPhoneX, 60 | isIOSChrome, 61 | }; 62 | } 63 | 64 | const detect = /*#__PURE__*/ detectIOS(); 65 | 66 | export const isIPad = /*#__PURE__*/ (() => detect.isIPad)(); 67 | export const isIPhone = /*#__PURE__*/ (() => detect.isIPhone)(); 68 | export const isIOS = /*#__PURE__*/ (() => detect.isIOS)(); 69 | export const isIPadOS = /*#__PURE__*/ (() => detect.isIPadOS)(); 70 | export const iosMajor = /*#__PURE__*/ (() => detect.iosMajor)(); 71 | export const iosMinor = /*#__PURE__*/ (() => detect.iosMinor)(); 72 | export const isWKWebView = /*#__PURE__*/ (() => detect.isWKWebView)(); 73 | export const isScrollBasedViewport = /*#__PURE__*/ (() => detect.isScrollBasedViewport)(); 74 | export const isIPhoneX = /*#__PURE__*/ (() => detect.isIPhoneX)(); 75 | export const isIOSChrome = /*#__PURE__*/ (() => detect.isIOSChrome)(); 76 | 77 | export function isLandscapePhone() { 78 | return Math.abs(window.orientation) === 90 && !isIPad; 79 | } 80 | 81 | // Reference: 82 | // https://stackoverflow.com/questions/28795476/detect-if-page-is-loaded-inside-wkwebview-in-javascript/30495399#30495399 83 | function checkWKWebView(ua: string) { 84 | if (!canUseDOM) { 85 | return false; 86 | } 87 | 88 | const webkit = (window as any).webkit; 89 | 90 | if (webkit && webkit.messageHandlers) { 91 | return true; 92 | } 93 | 94 | const lte9 = /constructor/i.test(String(window.HTMLElement)); 95 | const idb = !!window.indexedDB; 96 | 97 | if (ua.includes('safari') && ua.includes('version') && !(navigator as any).standalone) { 98 | // Safari (WKWebView/Nitro since 6+) 99 | } else if ((!idb && lte9) || !(window.statusbar && window.statusbar.visible)) { 100 | // UIWebView 101 | } else if (!lte9 || idb) { 102 | // WKWebView 103 | return true; 104 | } 105 | 106 | return false; 107 | } 108 | 109 | /** 110 | * В Safari на iPadOS поле User Agent содержит почти такую же информацию, что и в Safari на MacOS. 111 | * Из-за чего мы не можем ориентироваться на User Agent. 112 | * 113 | * Вместо этого мы пробуем определять есть ли событие 'ontouchend' в объекте document. 114 | * 115 | * см. https://developer.apple.com/forums/thread/119186?answerId=705140022#705140022 116 | */ 117 | export function checkIPadOS(ua: string) { 118 | if (!canUseDOM) { 119 | return false; 120 | } 121 | 122 | const isNotIOS = !/ipPad|iPhone|iPod/i.test(ua); 123 | const isMacOS = /Mac OS/i.test(ua); 124 | 125 | return isNotIOS && isMacOS && 'ontouchend' in document; 126 | } 127 | -------------------------------------------------------------------------------- /src/device/InputUtils.ts: -------------------------------------------------------------------------------- 1 | import { isIOS, isIPadOS } from './IOSDetections'; 2 | import { canUseDOM } from '../other/dom'; 3 | 4 | const detect = /*#__PURE__*/ (() => { 5 | const obj = { 6 | hasMouse: false, 7 | hasTouchEvents: false, 8 | hasHover: false, 9 | hasTouch: false, 10 | }; 11 | 12 | if (!canUseDOM) { 13 | return obj; 14 | } 15 | 16 | if (isIOS && !isIPadOS) { 17 | obj.hasMouse = false; 18 | obj.hasHover = false; 19 | obj.hasTouchEvents = true; 20 | obj.hasTouch = true; 21 | } else { 22 | obj.hasTouchEvents = 'ontouchstart' in document; 23 | obj.hasTouch = 24 | obj.hasTouchEvents || ('maxTouchPoints' in navigator && navigator.maxTouchPoints > 0); 25 | 26 | if (obj.hasTouch) { 27 | const notMobile = !/android|mobile|tablet/i.test(navigator.userAgent); 28 | 29 | obj.hasMouse = 30 | typeof window.matchMedia === 'function' && window.matchMedia('(pointer)').matches 31 | ? matchMedia('(pointer: fine)').matches 32 | : notMobile; 33 | 34 | obj.hasHover = 35 | obj.hasMouse && 36 | (typeof window.matchMedia === 'function' && window.matchMedia('(hover)').matches 37 | ? matchMedia('(hover: hover)').matches 38 | : notMobile); 39 | } else { 40 | obj.hasMouse = true; 41 | obj.hasHover = true; 42 | } 43 | } 44 | 45 | return obj; 46 | })(); 47 | 48 | export const hasMouse = /*#__PURE__*/ (() => detect.hasMouse)(); 49 | export const hasHover = /*#__PURE__*/ (() => detect.hasHover)(); 50 | export const hasTouchEvents = /*#__PURE__*/ (() => detect.hasTouchEvents)(); 51 | export const hasTouch = /*#__PURE__*/ (() => detect.hasTouch)(); 52 | -------------------------------------------------------------------------------- /src/device/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | IPHONE_SAFARI_BOTTOM_BAR, 3 | IPHONE_X_SAFARI_BOTTOM_BAR, 4 | IPHONE_KEYBOARD_REJECT_OFFSET, 5 | IOS_NO_KEYBOARD_ALLOWED_OFFSET, 6 | detectIOS, 7 | isIPad, 8 | isIPhone, 9 | isIOS, 10 | isIPadOS, 11 | iosMajor, 12 | iosMinor, 13 | isWKWebView, 14 | isScrollBasedViewport, 15 | isIPhoneX, 16 | isIOSChrome, 17 | isLandscapePhone, 18 | checkIPadOS, 19 | } from './IOSDetections'; 20 | 21 | export { hasMouse, hasHover, hasTouchEvents, hasTouch } from './InputUtils'; 22 | 23 | export { isRetina } from './retina'; 24 | -------------------------------------------------------------------------------- /src/device/retina.ts: -------------------------------------------------------------------------------- 1 | export function isRetina() { 2 | return window.devicePixelRatio >= 2; 3 | } 4 | -------------------------------------------------------------------------------- /src/html/__tests__/htmlEntities.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prettier/prettier */ 2 | import { expect, describe, test } from '@jest/globals'; 3 | import { 4 | escape, 5 | unescape, 6 | encodeHTMLEntities, 7 | decodeHTMLEntities, 8 | decodeHTMLEntitiesDeep, 9 | decodeHTMLFullEntities, 10 | } from '../escape'; 11 | 12 | const outOfBoundsChar = String.fromCharCode(65533); 13 | 14 | const empty = [ 15 | [ 16 | undefined, 17 | '', 18 | ], 19 | [ 20 | null, 21 | '', 22 | ], 23 | ] as string[][]; // JS type check 24 | 25 | const escapeTest = [ 26 | ...empty, 27 | [ 28 | 'Entities &<>\'"', 29 | 'Entities &<>'"', 30 | ], 31 | [ 32 | '&foo <> bar "fizz" l\'a', 33 | '&foo <> bar "fizz" l'a', 34 | ], 35 | ]; 36 | 37 | test.each(escapeTest)('escape(%j) should equal %j', (input, expected) => { 38 | expect(escape(input)).toEqual(expected); 39 | }); 40 | 41 | const unescapeTest = [ 42 | ...empty, 43 | [ 44 | 'Entities &<>'"', 45 | 'Entities &<>\'"', 46 | ], 47 | [ 48 | 'foo''bar', 49 | 'foo\'\'bar', 50 | ], 51 | ]; 52 | 53 | test.each(unescapeTest)('unescape(%j) should equal %j', (input, expected) => { 54 | expect(unescape(input)).toEqual(expected); 55 | }); 56 | 57 | const encodeTest = [ 58 | ...empty, 59 | [ 60 | 'a\n<>"\'&©∆℞😂\0\x01', 61 | 'a\n<>"'&©∆℞😂\x00', 62 | ], 63 | ]; 64 | 65 | test.each(encodeTest)('encodeHTMLEntities(%j) should equal %j', (input, expected) => { 66 | expect(encodeHTMLEntities(input)).toEqual(expected); 67 | }); 68 | 69 | const decodeTests = [ 70 | ...empty, 71 | [ 72 | 'Null and invalid entities � �', 73 | `Null and invalid entities ${outOfBoundsChar} ${outOfBoundsChar}`, 74 | ], 75 | [ 76 | 'Показувати моє ім'я лише авторові', 77 | 'Показувати моє ім\'я лише авторові', 78 | ], 79 | [ 80 | 'Երկիրը չգտնվեց', 81 | 'Երկիրը չգտնվեց', 82 | ], 83 | [ 84 | '& & < < > > " "', 85 | '& & < < > > " "', 86 | ], 87 | [ 88 | '" "', 89 | '" "', 90 | ], 91 | [ 92 | 'Привет!', 93 | 'Привет!', 94 | ], 95 | [ 96 | 'HEX entities ആ ആ', 97 | 'HEX entities ആ ആ', 98 | ], 99 | [ 100 | 'Emoji 😂 🧚', 101 | 'Emoji 😂 🧚', 102 | ], 103 | [ 104 | 'a\n<>"'&©∆℞😂\x00', 105 | 'a\n<>"\'&©∆℞😂\0\x01', 106 | ], 107 | ]; 108 | 109 | test.each(decodeTests)('decodeHTMLEntities(%j) should equal %j', (input, expected) => { 110 | expect(decodeHTMLEntities(input)).toEqual(expected); 111 | }); 112 | 113 | describe('decodeHTMLEntitiesDeep', () => { 114 | const decodeTestsLoopEntered = { 115 | array: ['Երկիրը չգտնվեց', { 116 | objectInArray: 'Показувати моє ім'я лише авторові', 117 | }], 118 | object: { 119 | keyInObject: '& & < < > > " "', 120 | arrayInObject: ['a\n<>"'&©∆℞😂\x00'], 121 | }, 122 | string: 'https://vk.com/groups?act=events_my', 123 | number: 123, 124 | function: test, 125 | null: null, 126 | undefined: undefined, 127 | }; 128 | 129 | const decodeTestsLoopExpected = { 130 | array: ['Երկիրը չգտնվեց', { 131 | objectInArray: 'Показувати моє ім\'я лише авторові', 132 | }], 133 | object: { 134 | keyInObject: '& & < < > > " "', 135 | arrayInObject: ['a\n<>"\'&©∆℞😂\0\x01'], 136 | }, 137 | string: 'https://vk.com/groups?act=events_my', 138 | number: 123, 139 | function: test, 140 | null: null, 141 | undefined: undefined, 142 | }; 143 | 144 | test('object input', () => { 145 | expect(decodeHTMLEntitiesDeep(decodeTestsLoopEntered)).toEqual(decodeTestsLoopExpected); 146 | }); 147 | 148 | test('string input', () => { 149 | expect(decodeHTMLEntitiesDeep('ր')).toEqual('ր'); 150 | }); 151 | 152 | test('number input', () => { 153 | expect(decodeHTMLEntitiesDeep(1)).toEqual(1); 154 | }); 155 | 156 | test('null input', () => { 157 | expect(decodeHTMLEntitiesDeep(null)).toEqual(null); 158 | }); 159 | 160 | test('undefined input', () => { 161 | expect(decodeHTMLEntitiesDeep(undefined)).toEqual(undefined); 162 | }); 163 | 164 | test('map input', () => { 165 | expect(decodeHTMLEntitiesDeep({ 'Ե': true })).toEqual({ Ե: true }); 166 | }); 167 | 168 | test('boolean input', () => { 169 | expect(decodeHTMLEntitiesDeep(false)).toEqual(false); 170 | }); 171 | 172 | test('function input', () => { 173 | expect(decodeHTMLEntitiesDeep(test)).toEqual(test); 174 | }); 175 | 176 | test('array input', () => { 177 | expect(decodeHTMLEntitiesDeep(['Ե', 1])).toEqual(['Ե', 1]); 178 | }); 179 | }); 180 | 181 | const decodeFullTests = [ 182 | ...decodeTests, 183 | [ 184 | '& &', 185 | '& &', 186 | ], 187 | [ 188 | 'text ⋛︀ blah', 189 | 'text \u22db\ufe00 blah', 190 | ], 191 | [ 192 | 'Lambda = λ = λ ', 193 | 'Lambda = λ = λ ', 194 | ], 195 | [ 196 | '&# &#x €43 © = ©f = ©', 197 | '&# &#x €43 © = ©f = ©', 198 | ], 199 | [ 200 | '& &', 201 | '& &', 202 | ], 203 | [ 204 | '&Pi Π', 205 | '&Pi Π', 206 | ], 207 | ]; 208 | 209 | test.each(decodeFullTests)('decodeHTMLFullEntities(%j) should equal %j', (input, expected) => { 210 | expect(decodeHTMLFullEntities(input)).toEqual(expected); 211 | }); 212 | -------------------------------------------------------------------------------- /src/html/entity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Эти сущности могут быть с `;` и без `;` на конце 3 | */ 4 | export const namedEntitiesWithoutSemicolon: Record = { 5 | AElig: '\u00C6', 6 | AMP: '\u0026', 7 | Aacute: '\u00C1', 8 | Agrave: '\u00C0', 9 | Aring: '\u00C5', 10 | Acirc: '\u00C2', 11 | Atilde: '\u00C3', 12 | Auml: '\u00C4', 13 | COPY: '\u00A9', 14 | Ccedil: '\u00C7', 15 | ETH: '\u00D0', 16 | Eacute: '\u00C9', 17 | Ecirc: '\u00CA', 18 | Egrave: '\u00C8', 19 | Euml: '\u00CB', 20 | GT: '\u003E', 21 | Iacute: '\u00CD', 22 | Icirc: '\u00CE', 23 | Igrave: '\u00CC', 24 | Iuml: '\u00CF', 25 | LT: '\u003C', 26 | Ntilde: '\u00D1', 27 | Oacute: '\u00D3', 28 | Ocirc: '\u00D4', 29 | Ograve: '\u00D2', 30 | Oslash: '\u00D8', 31 | Otilde: '\u00D5', 32 | Ouml: '\u00D6', 33 | QUOT: '\u0022', 34 | REG: '\u00AE', 35 | THORN: '\u00DE', 36 | Uacute: '\u00DA', 37 | Ucirc: '\u00DB', 38 | Ugrave: '\u00D9', 39 | Uuml: '\u00DC', 40 | Yacute: '\u00DD', 41 | aacute: '\u00E1', 42 | acirc: '\u00E2', 43 | acute: '\u00B4', 44 | aelig: '\u00E6', 45 | agrave: '\u00E0', 46 | amp: '\u0026', 47 | aring: '\u00E5', 48 | atilde: '\u00E3', 49 | auml: '\u00E4', 50 | brvbar: '\u00A6', 51 | ccedil: '\u00E7', 52 | cedil: '\u00B8', 53 | cent: '\u00A2', 54 | copy: '\u00A9', 55 | curren: '\u00A4', 56 | deg: '\u00B0', 57 | divide: '\u00F7', 58 | eacute: '\u00E9', 59 | ecirc: '\u00EA', 60 | egrave: '\u00E8', 61 | eth: '\u00F0', 62 | euml: '\u00EB', 63 | frac12: '\u00BD', 64 | frac14: '\u00BC', 65 | frac34: '\u00BE', 66 | gt: '\u003E', 67 | iacute: '\u00ED', 68 | icirc: '\u00EE', 69 | iexcl: '\u00A1', 70 | igrave: '\u00EC', 71 | iquest: '\u00BF', 72 | iuml: '\u00EF', 73 | laquo: '\u00AB', 74 | lt: '\u003C', 75 | macr: '\u00AF', 76 | micro: '\u00B5', 77 | middot: '\u00B7', 78 | nbsp: '\u00A0', 79 | not: '\u00AC', 80 | ntilde: '\u00F1', 81 | oacute: '\u00F3', 82 | ocirc: '\u00F4', 83 | ograve: '\u00F2', 84 | ordf: '\u00AA', 85 | ordm: '\u00BA', 86 | oslash: '\u00F8', 87 | otilde: '\u00F5', 88 | ouml: '\u00F6', 89 | para: '\u00B6', 90 | plusmn: '\u00B1', 91 | pound: '\u00A3', 92 | quot: '\u0022', 93 | raquo: '\u00BB', 94 | reg: '\u00AE', 95 | sect: '\u00A7', 96 | shy: '\u00AD', 97 | sup1: '\u00B9', 98 | sup2: '\u00B2', 99 | sup3: '\u00B3', 100 | szlig: '\u00DF', 101 | thorn: '\u00FE', 102 | times: '\u00D7', 103 | uacute: '\u00FA', 104 | ucirc: '\u00FB', 105 | ugrave: '\u00F9', 106 | uml: '\u00A8', 107 | uuml: '\u00FC', 108 | yacute: '\u00FD', 109 | yen: '\u00A5', 110 | yuml: '\u00FF', 111 | }; 112 | 113 | const namedEntities: Record = { 114 | Abreve: '\u0102', 115 | Acy: '\u0410', 116 | Afr: '\uD835\uDD04', 117 | Alpha: '\u0391', 118 | Amacr: '\u0100', 119 | And: '\u2A53', 120 | Aogon: '\u0104', 121 | Aopf: '\uD835\uDD38', 122 | ApplyFunction: '\u2061', 123 | Ascr: '\uD835\uDC9C', 124 | Assign: '\u2254', 125 | Backslash: '\u2216', 126 | Barv: '\u2AE7', 127 | Barwed: '\u2306', 128 | Bcy: '\u0411', 129 | Because: '\u2235', 130 | Bernoullis: '\u212C', 131 | Beta: '\u0392', 132 | Bfr: '\uD835\uDD05', 133 | Bopf: '\uD835\uDD39', 134 | Breve: '\u02D8', 135 | Bscr: '\u212C', 136 | Bumpeq: '\u224E', 137 | CHcy: '\u0427', 138 | Cacute: '\u0106', 139 | Cap: '\u22D2', 140 | CapitalDifferentialD: '\u2145', 141 | Cayleys: '\u212D', 142 | Ccaron: '\u010C', 143 | Ccirc: '\u0108', 144 | Cconint: '\u2230', 145 | Cdot: '\u010A', 146 | Cedilla: '\u00B8', 147 | CenterDot: '\u00B7', 148 | Cfr: '\u212D', 149 | Chi: '\u03A7', 150 | CircleDot: '\u2299', 151 | CircleMinus: '\u2296', 152 | CirclePlus: '\u2295', 153 | CircleTimes: '\u2297', 154 | ClockwiseContourIntegral: '\u2232', 155 | CloseCurlyDoubleQuote: '\u201D', 156 | CloseCurlyQuote: '\u2019', 157 | Colon: '\u2237', 158 | Colone: '\u2A74', 159 | Congruent: '\u2261', 160 | Conint: '\u222F', 161 | ContourIntegral: '\u222E', 162 | Copf: '\u2102', 163 | Coproduct: '\u2210', 164 | CounterClockwiseContourIntegral: '\u2233', 165 | Cross: '\u2A2F', 166 | Cscr: '\uD835\uDC9E', 167 | Cup: '\u22D3', 168 | CupCap: '\u224D', 169 | DD: '\u2145', 170 | DDotrahd: '\u2911', 171 | DJcy: '\u0402', 172 | DScy: '\u0405', 173 | DZcy: '\u040F', 174 | Dagger: '\u2021', 175 | Darr: '\u21A1', 176 | Dashv: '\u2AE4', 177 | Dcaron: '\u010E', 178 | Dcy: '\u0414', 179 | Del: '\u2207', 180 | Delta: '\u0394', 181 | Dfr: '\uD835\uDD07', 182 | DiacriticalAcute: '\u00B4', 183 | DiacriticalDot: '\u02D9', 184 | DiacriticalDoubleAcute: '\u02DD', 185 | DiacriticalGrave: '\u0060', 186 | DiacriticalTilde: '\u02DC', 187 | Diamond: '\u22C4', 188 | DifferentialD: '\u2146', 189 | Dopf: '\uD835\uDD3B', 190 | Dot: '\u00A8', 191 | DotDot: '\u20DC', 192 | DotEqual: '\u2250', 193 | DoubleContourIntegral: '\u222F', 194 | DoubleDot: '\u00A8', 195 | DoubleDownArrow: '\u21D3', 196 | DoubleLeftArrow: '\u21D0', 197 | DoubleLeftRightArrow: '\u21D4', 198 | DoubleLeftTee: '\u2AE4', 199 | DoubleLongLeftArrow: '\u27F8', 200 | DoubleLongLeftRightArrow: '\u27FA', 201 | DoubleLongRightArrow: '\u27F9', 202 | DoubleRightArrow: '\u21D2', 203 | DoubleRightTee: '\u22A8', 204 | DoubleUpArrow: '\u21D1', 205 | DoubleUpDownArrow: '\u21D5', 206 | DoubleVerticalBar: '\u2225', 207 | DownArrow: '\u2193', 208 | DownArrowBar: '\u2913', 209 | DownArrowUpArrow: '\u21F5', 210 | DownBreve: '\u0311', 211 | DownLeftRightVector: '\u2950', 212 | DownLeftTeeVector: '\u295E', 213 | DownLeftVector: '\u21BD', 214 | DownLeftVectorBar: '\u2956', 215 | DownRightTeeVector: '\u295F', 216 | DownRightVector: '\u21C1', 217 | DownRightVectorBar: '\u2957', 218 | DownTee: '\u22A4', 219 | DownTeeArrow: '\u21A7', 220 | Downarrow: '\u21D3', 221 | Dscr: '\uD835\uDC9F', 222 | Dstrok: '\u0110', 223 | ENG: '\u014A', 224 | Ecaron: '\u011A', 225 | Ecy: '\u042D', 226 | Edot: '\u0116', 227 | Efr: '\uD835\uDD08', 228 | Element: '\u2208', 229 | Emacr: '\u0112', 230 | EmptySmallSquare: '\u25FB', 231 | EmptyVerySmallSquare: '\u25AB', 232 | Eogon: '\u0118', 233 | Eopf: '\uD835\uDD3C', 234 | Epsilon: '\u0395', 235 | Equal: '\u2A75', 236 | EqualTilde: '\u2242', 237 | Equilibrium: '\u21CC', 238 | Escr: '\u2130', 239 | Esim: '\u2A73', 240 | Eta: '\u0397', 241 | Exists: '\u2203', 242 | ExponentialE: '\u2147', 243 | Fcy: '\u0424', 244 | Ffr: '\uD835\uDD09', 245 | FilledSmallSquare: '\u25FC', 246 | FilledVerySmallSquare: '\u25AA', 247 | Fopf: '\uD835\uDD3D', 248 | ForAll: '\u2200', 249 | Fouriertrf: '\u2131', 250 | Fscr: '\u2131', 251 | GJcy: '\u0403', 252 | Gamma: '\u0393', 253 | Gammad: '\u03DC', 254 | Gbreve: '\u011E', 255 | Gcedil: '\u0122', 256 | Gcirc: '\u011C', 257 | Gcy: '\u0413', 258 | Gdot: '\u0120', 259 | Gfr: '\uD835\uDD0A', 260 | Gg: '\u22D9', 261 | Gopf: '\uD835\uDD3E', 262 | GreaterEqual: '\u2265', 263 | GreaterEqualLess: '\u22DB', 264 | GreaterFullEqual: '\u2267', 265 | GreaterGreater: '\u2AA2', 266 | GreaterLess: '\u2277', 267 | GreaterSlantEqual: '\u2A7E', 268 | GreaterTilde: '\u2273', 269 | Gscr: '\uD835\uDCA2', 270 | Gt: '\u226B', 271 | HARDcy: '\u042A', 272 | Hacek: '\u02C7', 273 | Hat: '\u005E', 274 | Hcirc: '\u0124', 275 | Hfr: '\u210C', 276 | HilbertSpace: '\u210B', 277 | Hopf: '\u210D', 278 | HorizontalLine: '\u2500', 279 | Hscr: '\u210B', 280 | Hstrok: '\u0126', 281 | HumpDownHump: '\u224E', 282 | HumpEqual: '\u224F', 283 | IEcy: '\u0415', 284 | IJlig: '\u0132', 285 | IOcy: '\u0401', 286 | Icy: '\u0418', 287 | Idot: '\u0130', 288 | Ifr: '\u2111', 289 | Im: '\u2111', 290 | Imacr: '\u012A', 291 | ImaginaryI: '\u2148', 292 | Implies: '\u21D2', 293 | Int: '\u222C', 294 | Integral: '\u222B', 295 | Intersection: '\u22C2', 296 | InvisibleComma: '\u2063', 297 | InvisibleTimes: '\u2062', 298 | Iogon: '\u012E', 299 | Iopf: '\uD835\uDD40', 300 | Iota: '\u0399', 301 | Iscr: '\u2110', 302 | Itilde: '\u0128', 303 | Iukcy: '\u0406', 304 | Jcirc: '\u0134', 305 | Jcy: '\u0419', 306 | Jfr: '\uD835\uDD0D', 307 | Jopf: '\uD835\uDD41', 308 | Jscr: '\uD835\uDCA5', 309 | Jsercy: '\u0408', 310 | Jukcy: '\u0404', 311 | KHcy: '\u0425', 312 | KJcy: '\u040C', 313 | Kappa: '\u039A', 314 | Kcedil: '\u0136', 315 | Kcy: '\u041A', 316 | Kfr: '\uD835\uDD0E', 317 | Kopf: '\uD835\uDD42', 318 | Kscr: '\uD835\uDCA6', 319 | LJcy: '\u0409', 320 | Lacute: '\u0139', 321 | Lambda: '\u039B', 322 | Lang: '\u27EA', 323 | Laplacetrf: '\u2112', 324 | Larr: '\u219E', 325 | Lcaron: '\u013D', 326 | Lcedil: '\u013B', 327 | Lcy: '\u041B', 328 | LeftAngleBracket: '\u27E8', 329 | LeftArrow: '\u2190', 330 | LeftArrowBar: '\u21E4', 331 | LeftArrowRightArrow: '\u21C6', 332 | LeftCeiling: '\u2308', 333 | LeftDoubleBracket: '\u27E6', 334 | LeftDownTeeVector: '\u2961', 335 | LeftDownVector: '\u21C3', 336 | LeftDownVectorBar: '\u2959', 337 | LeftFloor: '\u230A', 338 | LeftRightArrow: '\u2194', 339 | LeftRightVector: '\u294E', 340 | LeftTee: '\u22A3', 341 | LeftTeeArrow: '\u21A4', 342 | LeftTeeVector: '\u295A', 343 | LeftTriangle: '\u22B2', 344 | LeftTriangleBar: '\u29CF', 345 | LeftTriangleEqual: '\u22B4', 346 | LeftUpDownVector: '\u2951', 347 | LeftUpTeeVector: '\u2960', 348 | LeftUpVector: '\u21BF', 349 | LeftUpVectorBar: '\u2958', 350 | LeftVector: '\u21BC', 351 | LeftVectorBar: '\u2952', 352 | Leftarrow: '\u21D0', 353 | Leftrightarrow: '\u21D4', 354 | LessEqualGreater: '\u22DA', 355 | LessFullEqual: '\u2266', 356 | LessGreater: '\u2276', 357 | LessLess: '\u2AA1', 358 | LessSlantEqual: '\u2A7D', 359 | LessTilde: '\u2272', 360 | Lfr: '\uD835\uDD0F', 361 | Ll: '\u22D8', 362 | Lleftarrow: '\u21DA', 363 | Lmidot: '\u013F', 364 | LongLeftArrow: '\u27F5', 365 | LongLeftRightArrow: '\u27F7', 366 | LongRightArrow: '\u27F6', 367 | Longleftarrow: '\u27F8', 368 | Longleftrightarrow: '\u27FA', 369 | Longrightarrow: '\u27F9', 370 | Lopf: '\uD835\uDD43', 371 | LowerLeftArrow: '\u2199', 372 | LowerRightArrow: '\u2198', 373 | Lscr: '\u2112', 374 | Lsh: '\u21B0', 375 | Lstrok: '\u0141', 376 | Lt: '\u226A', 377 | Map: '\u2905', 378 | Mcy: '\u041C', 379 | MediumSpace: '\u205F', 380 | Mellintrf: '\u2133', 381 | Mfr: '\uD835\uDD10', 382 | MinusPlus: '\u2213', 383 | Mopf: '\uD835\uDD44', 384 | Mscr: '\u2133', 385 | Mu: '\u039C', 386 | NJcy: '\u040A', 387 | Nacute: '\u0143', 388 | Ncaron: '\u0147', 389 | Ncedil: '\u0145', 390 | Ncy: '\u041D', 391 | NegativeMediumSpace: '\u200B', 392 | NegativeThickSpace: '\u200B', 393 | NegativeThinSpace: '\u200B', 394 | NegativeVeryThinSpace: '\u200B', 395 | NestedGreaterGreater: '\u226B', 396 | NestedLessLess: '\u226A', 397 | NewLine: '\u000A', 398 | Nfr: '\uD835\uDD11', 399 | NoBreak: '\u2060', 400 | NonBreakingSpace: '\u00A0', 401 | Nopf: '\u2115', 402 | Not: '\u2AEC', 403 | NotCongruent: '\u2262', 404 | NotCupCap: '\u226D', 405 | NotDoubleVerticalBar: '\u2226', 406 | NotElement: '\u2209', 407 | NotEqual: '\u2260', 408 | NotEqualTilde: '\u2242\u0338', 409 | NotExists: '\u2204', 410 | NotGreater: '\u226F', 411 | NotGreaterEqual: '\u2271', 412 | NotGreaterFullEqual: '\u2267\u0338', 413 | NotGreaterGreater: '\u226B\u0338', 414 | NotGreaterLess: '\u2279', 415 | NotGreaterSlantEqual: '\u2A7E\u0338', 416 | NotGreaterTilde: '\u2275', 417 | NotHumpDownHump: '\u224E\u0338', 418 | NotHumpEqual: '\u224F\u0338', 419 | NotLeftTriangle: '\u22EA', 420 | NotLeftTriangleBar: '\u29CF\u0338', 421 | NotLeftTriangleEqual: '\u22EC', 422 | NotLess: '\u226E', 423 | NotLessEqual: '\u2270', 424 | NotLessGreater: '\u2278', 425 | NotLessLess: '\u226A\u0338', 426 | NotLessSlantEqual: '\u2A7D\u0338', 427 | NotLessTilde: '\u2274', 428 | NotNestedGreaterGreater: '\u2AA2\u0338', 429 | NotNestedLessLess: '\u2AA1\u0338', 430 | NotPrecedes: '\u2280', 431 | NotPrecedesEqual: '\u2AAF\u0338', 432 | NotPrecedesSlantEqual: '\u22E0', 433 | NotReverseElement: '\u220C', 434 | NotRightTriangle: '\u22EB', 435 | NotRightTriangleBar: '\u29D0\u0338', 436 | NotRightTriangleEqual: '\u22ED', 437 | NotSquareSubset: '\u228F\u0338', 438 | NotSquareSubsetEqual: '\u22E2', 439 | NotSquareSuperset: '\u2290\u0338', 440 | NotSquareSupersetEqual: '\u22E3', 441 | NotSubset: '\u2282\u20D2', 442 | NotSubsetEqual: '\u2288', 443 | NotSucceeds: '\u2281', 444 | NotSucceedsEqual: '\u2AB0\u0338', 445 | NotSucceedsSlantEqual: '\u22E1', 446 | NotSucceedsTilde: '\u227F\u0338', 447 | NotSuperset: '\u2283\u20D2', 448 | NotSupersetEqual: '\u2289', 449 | NotTilde: '\u2241', 450 | NotTildeEqual: '\u2244', 451 | NotTildeFullEqual: '\u2247', 452 | NotTildeTilde: '\u2249', 453 | NotVerticalBar: '\u2224', 454 | Nscr: '\uD835\uDCA9', 455 | Nu: '\u039D', 456 | OElig: '\u0152', 457 | Ocy: '\u041E', 458 | Odblac: '\u0150', 459 | Ofr: '\uD835\uDD12', 460 | Omacr: '\u014C', 461 | Omega: '\u03A9', 462 | Omicron: '\u039F', 463 | Oopf: '\uD835\uDD46', 464 | OpenCurlyDoubleQuote: '\u201C', 465 | OpenCurlyQuote: '\u2018', 466 | Or: '\u2A54', 467 | Oscr: '\uD835\uDCAA', 468 | Otimes: '\u2A37', 469 | OverBar: '\u203E', 470 | OverBrace: '\u23DE', 471 | OverBracket: '\u23B4', 472 | OverParenthesis: '\u23DC', 473 | PartialD: '\u2202', 474 | Pcy: '\u041F', 475 | Pfr: '\uD835\uDD13', 476 | Phi: '\u03A6', 477 | Pi: '\u03A0', 478 | PlusMinus: '\u00B1', 479 | Poincareplane: '\u210C', 480 | Popf: '\u2119', 481 | Pr: '\u2ABB', 482 | Precedes: '\u227A', 483 | PrecedesEqual: '\u2AAF', 484 | PrecedesSlantEqual: '\u227C', 485 | PrecedesTilde: '\u227E', 486 | Prime: '\u2033', 487 | Product: '\u220F', 488 | Proportion: '\u2237', 489 | Proportional: '\u221D', 490 | Pscr: '\uD835\uDCAB', 491 | Psi: '\u03A8', 492 | Qfr: '\uD835\uDD14', 493 | Qopf: '\u211A', 494 | Qscr: '\uD835\uDCAC', 495 | RBarr: '\u2910', 496 | Racute: '\u0154', 497 | Rang: '\u27EB', 498 | Rarr: '\u21A0', 499 | Rarrtl: '\u2916', 500 | Rcaron: '\u0158', 501 | Rcedil: '\u0156', 502 | Rcy: '\u0420', 503 | Re: '\u211C', 504 | ReverseElement: '\u220B', 505 | ReverseEquilibrium: '\u21CB', 506 | ReverseUpEquilibrium: '\u296F', 507 | Rfr: '\u211C', 508 | Rho: '\u03A1', 509 | RightAngleBracket: '\u27E9', 510 | RightArrow: '\u2192', 511 | RightArrowBar: '\u21E5', 512 | RightArrowLeftArrow: '\u21C4', 513 | RightCeiling: '\u2309', 514 | RightDoubleBracket: '\u27E7', 515 | RightDownTeeVector: '\u295D', 516 | RightDownVector: '\u21C2', 517 | RightDownVectorBar: '\u2955', 518 | RightFloor: '\u230B', 519 | RightTee: '\u22A2', 520 | RightTeeArrow: '\u21A6', 521 | RightTeeVector: '\u295B', 522 | RightTriangle: '\u22B3', 523 | RightTriangleBar: '\u29D0', 524 | RightTriangleEqual: '\u22B5', 525 | RightUpDownVector: '\u294F', 526 | RightUpTeeVector: '\u295C', 527 | RightUpVector: '\u21BE', 528 | RightUpVectorBar: '\u2954', 529 | RightVector: '\u21C0', 530 | RightVectorBar: '\u2953', 531 | Rightarrow: '\u21D2', 532 | Ropf: '\u211D', 533 | RoundImplies: '\u2970', 534 | Rrightarrow: '\u21DB', 535 | Rscr: '\u211B', 536 | Rsh: '\u21B1', 537 | RuleDelayed: '\u29F4', 538 | SHCHcy: '\u0429', 539 | SHcy: '\u0428', 540 | SOFTcy: '\u042C', 541 | Sacute: '\u015A', 542 | Sc: '\u2ABC', 543 | Scaron: '\u0160', 544 | Scedil: '\u015E', 545 | Scirc: '\u015C', 546 | Scy: '\u0421', 547 | Sfr: '\uD835\uDD16', 548 | ShortDownArrow: '\u2193', 549 | ShortLeftArrow: '\u2190', 550 | ShortRightArrow: '\u2192', 551 | ShortUpArrow: '\u2191', 552 | Sigma: '\u03A3', 553 | SmallCircle: '\u2218', 554 | Sopf: '\uD835\uDD4A', 555 | Sqrt: '\u221A', 556 | Square: '\u25A1', 557 | SquareIntersection: '\u2293', 558 | SquareSubset: '\u228F', 559 | SquareSubsetEqual: '\u2291', 560 | SquareSuperset: '\u2290', 561 | SquareSupersetEqual: '\u2292', 562 | SquareUnion: '\u2294', 563 | Sscr: '\uD835\uDCAE', 564 | Star: '\u22C6', 565 | Sub: '\u22D0', 566 | Subset: '\u22D0', 567 | SubsetEqual: '\u2286', 568 | Succeeds: '\u227B', 569 | SucceedsEqual: '\u2AB0', 570 | SucceedsSlantEqual: '\u227D', 571 | SucceedsTilde: '\u227F', 572 | SuchThat: '\u220B', 573 | Sum: '\u2211', 574 | Sup: '\u22D1', 575 | Superset: '\u2283', 576 | SupersetEqual: '\u2287', 577 | Supset: '\u22D1', 578 | TRADE: '\u2122', 579 | TSHcy: '\u040B', 580 | TScy: '\u0426', 581 | Tab: '\u0009', 582 | Tau: '\u03A4', 583 | Tcaron: '\u0164', 584 | Tcedil: '\u0162', 585 | Tcy: '\u0422', 586 | Tfr: '\uD835\uDD17', 587 | Therefore: '\u2234', 588 | Theta: '\u0398', 589 | ThickSpace: '\u205F\u200A', 590 | ThinSpace: '\u2009', 591 | Tilde: '\u223C', 592 | TildeEqual: '\u2243', 593 | TildeFullEqual: '\u2245', 594 | TildeTilde: '\u2248', 595 | Topf: '\uD835\uDD4B', 596 | TripleDot: '\u20DB', 597 | Tscr: '\uD835\uDCAF', 598 | Tstrok: '\u0166', 599 | Uarr: '\u219F', 600 | Uarrocir: '\u2949', 601 | Ubrcy: '\u040E', 602 | Ubreve: '\u016C', 603 | Ucy: '\u0423', 604 | Udblac: '\u0170', 605 | Ufr: '\uD835\uDD18', 606 | Umacr: '\u016A', 607 | UnderBar: '\u005F', 608 | UnderBrace: '\u23DF', 609 | UnderBracket: '\u23B5', 610 | UnderParenthesis: '\u23DD', 611 | Union: '\u22C3', 612 | UnionPlus: '\u228E', 613 | Uogon: '\u0172', 614 | Uopf: '\uD835\uDD4C', 615 | UpArrow: '\u2191', 616 | UpArrowBar: '\u2912', 617 | UpArrowDownArrow: '\u21C5', 618 | UpDownArrow: '\u2195', 619 | UpEquilibrium: '\u296E', 620 | UpTee: '\u22A5', 621 | UpTeeArrow: '\u21A5', 622 | Uparrow: '\u21D1', 623 | Updownarrow: '\u21D5', 624 | UpperLeftArrow: '\u2196', 625 | UpperRightArrow: '\u2197', 626 | Upsi: '\u03D2', 627 | Upsilon: '\u03A5', 628 | Uring: '\u016E', 629 | Uscr: '\uD835\uDCB0', 630 | Utilde: '\u0168', 631 | VDash: '\u22AB', 632 | Vbar: '\u2AEB', 633 | Vcy: '\u0412', 634 | Vdash: '\u22A9', 635 | Vdashl: '\u2AE6', 636 | Vee: '\u22C1', 637 | Verbar: '\u2016', 638 | Vert: '\u2016', 639 | VerticalBar: '\u2223', 640 | VerticalLine: '\u007C', 641 | VerticalSeparator: '\u2758', 642 | VerticalTilde: '\u2240', 643 | VeryThinSpace: '\u200A', 644 | Vfr: '\uD835\uDD19', 645 | Vopf: '\uD835\uDD4D', 646 | Vscr: '\uD835\uDCB1', 647 | Vvdash: '\u22AA', 648 | Wcirc: '\u0174', 649 | Wedge: '\u22C0', 650 | Wfr: '\uD835\uDD1A', 651 | Wopf: '\uD835\uDD4E', 652 | Wscr: '\uD835\uDCB2', 653 | Xfr: '\uD835\uDD1B', 654 | Xi: '\u039E', 655 | Xopf: '\uD835\uDD4F', 656 | Xscr: '\uD835\uDCB3', 657 | YAcy: '\u042F', 658 | YIcy: '\u0407', 659 | YUcy: '\u042E', 660 | Ycirc: '\u0176', 661 | Ycy: '\u042B', 662 | Yfr: '\uD835\uDD1C', 663 | Yopf: '\uD835\uDD50', 664 | Yscr: '\uD835\uDCB4', 665 | Yuml: '\u0178', 666 | ZHcy: '\u0416', 667 | Zacute: '\u0179', 668 | Zcaron: '\u017D', 669 | Zcy: '\u0417', 670 | Zdot: '\u017B', 671 | ZeroWidthSpace: '\u200B', 672 | Zeta: '\u0396', 673 | Zfr: '\u2128', 674 | Zopf: '\u2124', 675 | Zscr: '\uD835\uDCB5', 676 | abreve: '\u0103', 677 | ac: '\u223E', 678 | acE: '\u223E\u0333', 679 | acd: '\u223F', 680 | acy: '\u0430', 681 | af: '\u2061', 682 | afr: '\uD835\uDD1E', 683 | alefsym: '\u2135', 684 | aleph: '\u2135', 685 | alpha: '\u03B1', 686 | amacr: '\u0101', 687 | amalg: '\u2A3F', 688 | and: '\u2227', 689 | andand: '\u2A55', 690 | andd: '\u2A5C', 691 | andslope: '\u2A58', 692 | andv: '\u2A5A', 693 | ang: '\u2220', 694 | ange: '\u29A4', 695 | angle: '\u2220', 696 | angmsd: '\u2221', 697 | angmsdaa: '\u29A8', 698 | angmsdab: '\u29A9', 699 | angmsdac: '\u29AA', 700 | angmsdad: '\u29AB', 701 | angmsdae: '\u29AC', 702 | angmsdaf: '\u29AD', 703 | angmsdag: '\u29AE', 704 | angmsdah: '\u29AF', 705 | angrt: '\u221F', 706 | angrtvb: '\u22BE', 707 | angrtvbd: '\u299D', 708 | angsph: '\u2222', 709 | angst: '\u00C5', 710 | angzarr: '\u237C', 711 | aogon: '\u0105', 712 | aopf: '\uD835\uDD52', 713 | ap: '\u2248', 714 | apE: '\u2A70', 715 | apacir: '\u2A6F', 716 | ape: '\u224A', 717 | apid: '\u224B', 718 | apos: '\u0027', 719 | approx: '\u2248', 720 | approxeq: '\u224A', 721 | ascr: '\uD835\uDCB6', 722 | ast: '\u002A', 723 | asymp: '\u2248', 724 | asympeq: '\u224D', 725 | awconint: '\u2233', 726 | awint: '\u2A11', 727 | bNot: '\u2AED', 728 | backcong: '\u224C', 729 | backepsilon: '\u03F6', 730 | backprime: '\u2035', 731 | backsim: '\u223D', 732 | backsimeq: '\u22CD', 733 | barvee: '\u22BD', 734 | barwed: '\u2305', 735 | barwedge: '\u2305', 736 | bbrk: '\u23B5', 737 | bbrktbrk: '\u23B6', 738 | bcong: '\u224C', 739 | bcy: '\u0431', 740 | bdquo: '\u201E', 741 | becaus: '\u2235', 742 | because: '\u2235', 743 | bemptyv: '\u29B0', 744 | bepsi: '\u03F6', 745 | bernou: '\u212C', 746 | beta: '\u03B2', 747 | beth: '\u2136', 748 | between: '\u226C', 749 | bfr: '\uD835\uDD1F', 750 | bigcap: '\u22C2', 751 | bigcirc: '\u25EF', 752 | bigcup: '\u22C3', 753 | bigodot: '\u2A00', 754 | bigoplus: '\u2A01', 755 | bigotimes: '\u2A02', 756 | bigsqcup: '\u2A06', 757 | bigstar: '\u2605', 758 | bigtriangledown: '\u25BD', 759 | bigtriangleup: '\u25B3', 760 | biguplus: '\u2A04', 761 | bigvee: '\u22C1', 762 | bigwedge: '\u22C0', 763 | bkarow: '\u290D', 764 | blacklozenge: '\u29EB', 765 | blacksquare: '\u25AA', 766 | blacktriangle: '\u25B4', 767 | blacktriangledown: '\u25BE', 768 | blacktriangleleft: '\u25C2', 769 | blacktriangleright: '\u25B8', 770 | blank: '\u2423', 771 | blk12: '\u2592', 772 | blk14: '\u2591', 773 | blk34: '\u2593', 774 | block: '\u2588', 775 | bne: '\u003D\u20E5', 776 | bnequiv: '\u2261\u20E5', 777 | bnot: '\u2310', 778 | bopf: '\uD835\uDD53', 779 | bot: '\u22A5', 780 | bottom: '\u22A5', 781 | bowtie: '\u22C8', 782 | boxDL: '\u2557', 783 | boxDR: '\u2554', 784 | boxDl: '\u2556', 785 | boxDr: '\u2553', 786 | boxH: '\u2550', 787 | boxHD: '\u2566', 788 | boxHU: '\u2569', 789 | boxHd: '\u2564', 790 | boxHu: '\u2567', 791 | boxUL: '\u255D', 792 | boxUR: '\u255A', 793 | boxUl: '\u255C', 794 | boxUr: '\u2559', 795 | boxV: '\u2551', 796 | boxVH: '\u256C', 797 | boxVL: '\u2563', 798 | boxVR: '\u2560', 799 | boxVh: '\u256B', 800 | boxVl: '\u2562', 801 | boxVr: '\u255F', 802 | boxbox: '\u29C9', 803 | boxdL: '\u2555', 804 | boxdR: '\u2552', 805 | boxdl: '\u2510', 806 | boxdr: '\u250C', 807 | boxh: '\u2500', 808 | boxhD: '\u2565', 809 | boxhU: '\u2568', 810 | boxhd: '\u252C', 811 | boxhu: '\u2534', 812 | boxminus: '\u229F', 813 | boxplus: '\u229E', 814 | boxtimes: '\u22A0', 815 | boxuL: '\u255B', 816 | boxuR: '\u2558', 817 | boxul: '\u2518', 818 | boxur: '\u2514', 819 | boxv: '\u2502', 820 | boxvH: '\u256A', 821 | boxvL: '\u2561', 822 | boxvR: '\u255E', 823 | boxvh: '\u253C', 824 | boxvl: '\u2524', 825 | boxvr: '\u251C', 826 | bprime: '\u2035', 827 | breve: '\u02D8', 828 | bscr: '\uD835\uDCB7', 829 | bsemi: '\u204F', 830 | bsim: '\u223D', 831 | bsime: '\u22CD', 832 | bsol: '\u005C', 833 | bsolb: '\u29C5', 834 | bsolhsub: '\u27C8', 835 | bull: '\u2022', 836 | bullet: '\u2022', 837 | bump: '\u224E', 838 | bumpE: '\u2AAE', 839 | bumpe: '\u224F', 840 | bumpeq: '\u224F', 841 | cacute: '\u0107', 842 | cap: '\u2229', 843 | capand: '\u2A44', 844 | capbrcup: '\u2A49', 845 | capcap: '\u2A4B', 846 | capcup: '\u2A47', 847 | capdot: '\u2A40', 848 | caps: '\u2229\uFE00', 849 | caret: '\u2041', 850 | caron: '\u02C7', 851 | ccaps: '\u2A4D', 852 | ccaron: '\u010D', 853 | ccirc: '\u0109', 854 | ccups: '\u2A4C', 855 | ccupssm: '\u2A50', 856 | cdot: '\u010B', 857 | cemptyv: '\u29B2', 858 | centerdot: '\u00B7', 859 | cfr: '\uD835\uDD20', 860 | chcy: '\u0447', 861 | check: '\u2713', 862 | checkmark: '\u2713', 863 | chi: '\u03C7', 864 | cir: '\u25CB', 865 | cirE: '\u29C3', 866 | circ: '\u02C6', 867 | circeq: '\u2257', 868 | circlearrowleft: '\u21BA', 869 | circlearrowright: '\u21BB', 870 | circledR: '\u00AE', 871 | circledS: '\u24C8', 872 | circledast: '\u229B', 873 | circledcirc: '\u229A', 874 | circleddash: '\u229D', 875 | cire: '\u2257', 876 | cirfnint: '\u2A10', 877 | cirmid: '\u2AEF', 878 | cirscir: '\u29C2', 879 | clubs: '\u2663', 880 | clubsuit: '\u2663', 881 | colon: '\u003A', 882 | colone: '\u2254', 883 | coloneq: '\u2254', 884 | comma: '\u002C', 885 | commat: '\u0040', 886 | comp: '\u2201', 887 | compfn: '\u2218', 888 | complement: '\u2201', 889 | complexes: '\u2102', 890 | cong: '\u2245', 891 | congdot: '\u2A6D', 892 | conint: '\u222E', 893 | copf: '\uD835\uDD54', 894 | coprod: '\u2210', 895 | copysr: '\u2117', 896 | crarr: '\u21B5', 897 | cross: '\u2717', 898 | cscr: '\uD835\uDCB8', 899 | csub: '\u2ACF', 900 | csube: '\u2AD1', 901 | csup: '\u2AD0', 902 | csupe: '\u2AD2', 903 | ctdot: '\u22EF', 904 | cudarrl: '\u2938', 905 | cudarrr: '\u2935', 906 | cuepr: '\u22DE', 907 | cuesc: '\u22DF', 908 | cularr: '\u21B6', 909 | cularrp: '\u293D', 910 | cup: '\u222A', 911 | cupbrcap: '\u2A48', 912 | cupcap: '\u2A46', 913 | cupcup: '\u2A4A', 914 | cupdot: '\u228D', 915 | cupor: '\u2A45', 916 | cups: '\u222A\uFE00', 917 | curarr: '\u21B7', 918 | curarrm: '\u293C', 919 | curlyeqprec: '\u22DE', 920 | curlyeqsucc: '\u22DF', 921 | curlyvee: '\u22CE', 922 | curlywedge: '\u22CF', 923 | curvearrowleft: '\u21B6', 924 | curvearrowright: '\u21B7', 925 | cuvee: '\u22CE', 926 | cuwed: '\u22CF', 927 | cwconint: '\u2232', 928 | cwint: '\u2231', 929 | cylcty: '\u232D', 930 | dArr: '\u21D3', 931 | dHar: '\u2965', 932 | dagger: '\u2020', 933 | daleth: '\u2138', 934 | darr: '\u2193', 935 | dash: '\u2010', 936 | dashv: '\u22A3', 937 | dbkarow: '\u290F', 938 | dblac: '\u02DD', 939 | dcaron: '\u010F', 940 | dcy: '\u0434', 941 | dd: '\u2146', 942 | ddagger: '\u2021', 943 | ddarr: '\u21CA', 944 | ddotseq: '\u2A77', 945 | delta: '\u03B4', 946 | demptyv: '\u29B1', 947 | dfisht: '\u297F', 948 | dfr: '\uD835\uDD21', 949 | dharl: '\u21C3', 950 | dharr: '\u21C2', 951 | diam: '\u22C4', 952 | diamond: '\u22C4', 953 | diamondsuit: '\u2666', 954 | diams: '\u2666', 955 | die: '\u00A8', 956 | digamma: '\u03DD', 957 | disin: '\u22F2', 958 | div: '\u00F7', 959 | divideontimes: '\u22C7', 960 | divonx: '\u22C7', 961 | djcy: '\u0452', 962 | dlcorn: '\u231E', 963 | dlcrop: '\u230D', 964 | dollar: '\u0024', 965 | dopf: '\uD835\uDD55', 966 | dot: '\u02D9', 967 | doteq: '\u2250', 968 | doteqdot: '\u2251', 969 | dotminus: '\u2238', 970 | dotplus: '\u2214', 971 | dotsquare: '\u22A1', 972 | doublebarwedge: '\u2306', 973 | downarrow: '\u2193', 974 | downdownarrows: '\u21CA', 975 | downharpoonleft: '\u21C3', 976 | downharpoonright: '\u21C2', 977 | drbkarow: '\u2910', 978 | drcorn: '\u231F', 979 | drcrop: '\u230C', 980 | dscr: '\uD835\uDCB9', 981 | dscy: '\u0455', 982 | dsol: '\u29F6', 983 | dstrok: '\u0111', 984 | dtdot: '\u22F1', 985 | dtri: '\u25BF', 986 | dtrif: '\u25BE', 987 | duarr: '\u21F5', 988 | duhar: '\u296F', 989 | dwangle: '\u29A6', 990 | dzcy: '\u045F', 991 | dzigrarr: '\u27FF', 992 | eDDot: '\u2A77', 993 | eDot: '\u2251', 994 | easter: '\u2A6E', 995 | ecaron: '\u011B', 996 | ecir: '\u2256', 997 | ecolon: '\u2255', 998 | ecy: '\u044D', 999 | edot: '\u0117', 1000 | ee: '\u2147', 1001 | efDot: '\u2252', 1002 | efr: '\uD835\uDD22', 1003 | eg: '\u2A9A', 1004 | egs: '\u2A96', 1005 | egsdot: '\u2A98', 1006 | el: '\u2A99', 1007 | elinters: '\u23E7', 1008 | ell: '\u2113', 1009 | els: '\u2A95', 1010 | elsdot: '\u2A97', 1011 | emacr: '\u0113', 1012 | empty: '\u2205', 1013 | emptyset: '\u2205', 1014 | emptyv: '\u2205', 1015 | emsp13: '\u2004', 1016 | emsp14: '\u2005', 1017 | emsp: '\u2003', 1018 | eng: '\u014B', 1019 | ensp: '\u2002', 1020 | eogon: '\u0119', 1021 | eopf: '\uD835\uDD56', 1022 | epar: '\u22D5', 1023 | eparsl: '\u29E3', 1024 | eplus: '\u2A71', 1025 | epsi: '\u03B5', 1026 | epsilon: '\u03B5', 1027 | epsiv: '\u03F5', 1028 | eqcirc: '\u2256', 1029 | eqcolon: '\u2255', 1030 | eqsim: '\u2242', 1031 | eqslantgtr: '\u2A96', 1032 | eqslantless: '\u2A95', 1033 | equals: '\u003D', 1034 | equest: '\u225F', 1035 | equiv: '\u2261', 1036 | equivDD: '\u2A78', 1037 | eqvparsl: '\u29E5', 1038 | erDot: '\u2253', 1039 | erarr: '\u2971', 1040 | escr: '\u212F', 1041 | esdot: '\u2250', 1042 | esim: '\u2242', 1043 | eta: '\u03B7', 1044 | euro: '\u20AC', 1045 | excl: '\u0021', 1046 | exist: '\u2203', 1047 | expectation: '\u2130', 1048 | exponentiale: '\u2147', 1049 | fallingdotseq: '\u2252', 1050 | fcy: '\u0444', 1051 | female: '\u2640', 1052 | ffilig: '\uFB03', 1053 | fflig: '\uFB00', 1054 | ffllig: '\uFB04', 1055 | ffr: '\uD835\uDD23', 1056 | filig: '\uFB01', 1057 | fjlig: '\u0066\u006A', 1058 | flat: '\u266D', 1059 | fllig: '\uFB02', 1060 | fltns: '\u25B1', 1061 | fnof: '\u0192', 1062 | fopf: '\uD835\uDD57', 1063 | forall: '\u2200', 1064 | fork: '\u22D4', 1065 | forkv: '\u2AD9', 1066 | fpartint: '\u2A0D', 1067 | frac13: '\u2153', 1068 | frac15: '\u2155', 1069 | frac16: '\u2159', 1070 | frac18: '\u215B', 1071 | frac23: '\u2154', 1072 | frac25: '\u2156', 1073 | frac35: '\u2157', 1074 | frac38: '\u215C', 1075 | frac45: '\u2158', 1076 | frac56: '\u215A', 1077 | frac58: '\u215D', 1078 | frac78: '\u215E', 1079 | frasl: '\u2044', 1080 | frown: '\u2322', 1081 | fscr: '\uD835\uDCBB', 1082 | gE: '\u2267', 1083 | gEl: '\u2A8C', 1084 | gacute: '\u01F5', 1085 | gamma: '\u03B3', 1086 | gammad: '\u03DD', 1087 | gap: '\u2A86', 1088 | gbreve: '\u011F', 1089 | gcirc: '\u011D', 1090 | gcy: '\u0433', 1091 | gdot: '\u0121', 1092 | ge: '\u2265', 1093 | gel: '\u22DB', 1094 | geq: '\u2265', 1095 | geqq: '\u2267', 1096 | geqslant: '\u2A7E', 1097 | ges: '\u2A7E', 1098 | gescc: '\u2AA9', 1099 | gesdot: '\u2A80', 1100 | gesdoto: '\u2A82', 1101 | gesdotol: '\u2A84', 1102 | gesl: '\u22DB\uFE00', 1103 | gesles: '\u2A94', 1104 | gfr: '\uD835\uDD24', 1105 | gg: '\u226B', 1106 | ggg: '\u22D9', 1107 | gimel: '\u2137', 1108 | gjcy: '\u0453', 1109 | gl: '\u2277', 1110 | glE: '\u2A92', 1111 | gla: '\u2AA5', 1112 | glj: '\u2AA4', 1113 | gnE: '\u2269', 1114 | gnap: '\u2A8A', 1115 | gnapprox: '\u2A8A', 1116 | gne: '\u2A88', 1117 | gneq: '\u2A88', 1118 | gneqq: '\u2269', 1119 | gnsim: '\u22E7', 1120 | gopf: '\uD835\uDD58', 1121 | grave: '\u0060', 1122 | gscr: '\u210A', 1123 | gsim: '\u2273', 1124 | gsime: '\u2A8E', 1125 | gsiml: '\u2A90', 1126 | gtcc: '\u2AA7', 1127 | gtcir: '\u2A7A', 1128 | gtdot: '\u22D7', 1129 | gtlPar: '\u2995', 1130 | gtquest: '\u2A7C', 1131 | gtrapprox: '\u2A86', 1132 | gtrarr: '\u2978', 1133 | gtrdot: '\u22D7', 1134 | gtreqless: '\u22DB', 1135 | gtreqqless: '\u2A8C', 1136 | gtrless: '\u2277', 1137 | gtrsim: '\u2273', 1138 | gvertneqq: '\u2269\uFE00', 1139 | gvnE: '\u2269\uFE00', 1140 | hArr: '\u21D4', 1141 | hairsp: '\u200A', 1142 | half: '\u00BD', 1143 | hamilt: '\u210B', 1144 | hardcy: '\u044A', 1145 | harr: '\u2194', 1146 | harrcir: '\u2948', 1147 | harrw: '\u21AD', 1148 | hbar: '\u210F', 1149 | hcirc: '\u0125', 1150 | hearts: '\u2665', 1151 | heartsuit: '\u2665', 1152 | hellip: '\u2026', 1153 | hercon: '\u22B9', 1154 | hfr: '\uD835\uDD25', 1155 | hksearow: '\u2925', 1156 | hkswarow: '\u2926', 1157 | hoarr: '\u21FF', 1158 | homtht: '\u223B', 1159 | hookleftarrow: '\u21A9', 1160 | hookrightarrow: '\u21AA', 1161 | hopf: '\uD835\uDD59', 1162 | horbar: '\u2015', 1163 | hscr: '\uD835\uDCBD', 1164 | hslash: '\u210F', 1165 | hstrok: '\u0127', 1166 | hybull: '\u2043', 1167 | hyphen: '\u2010', 1168 | ic: '\u2063', 1169 | icy: '\u0438', 1170 | iecy: '\u0435', 1171 | iff: '\u21D4', 1172 | ifr: '\uD835\uDD26', 1173 | ii: '\u2148', 1174 | iiiint: '\u2A0C', 1175 | iiint: '\u222D', 1176 | iinfin: '\u29DC', 1177 | iiota: '\u2129', 1178 | ijlig: '\u0133', 1179 | imacr: '\u012B', 1180 | image: '\u2111', 1181 | imagline: '\u2110', 1182 | imagpart: '\u2111', 1183 | imath: '\u0131', 1184 | imof: '\u22B7', 1185 | imped: '\u01B5', 1186 | in: '\u2208', 1187 | incare: '\u2105', 1188 | infin: '\u221E', 1189 | infintie: '\u29DD', 1190 | inodot: '\u0131', 1191 | int: '\u222B', 1192 | intcal: '\u22BA', 1193 | integers: '\u2124', 1194 | intercal: '\u22BA', 1195 | intlarhk: '\u2A17', 1196 | intprod: '\u2A3C', 1197 | iocy: '\u0451', 1198 | iogon: '\u012F', 1199 | iopf: '\uD835\uDD5A', 1200 | iota: '\u03B9', 1201 | iprod: '\u2A3C', 1202 | iscr: '\uD835\uDCBE', 1203 | isin: '\u2208', 1204 | isinE: '\u22F9', 1205 | isindot: '\u22F5', 1206 | isins: '\u22F4', 1207 | isinsv: '\u22F3', 1208 | isinv: '\u2208', 1209 | it: '\u2062', 1210 | itilde: '\u0129', 1211 | iukcy: '\u0456', 1212 | jcirc: '\u0135', 1213 | jcy: '\u0439', 1214 | jfr: '\uD835\uDD27', 1215 | jmath: '\u0237', 1216 | jopf: '\uD835\uDD5B', 1217 | jscr: '\uD835\uDCBF', 1218 | jsercy: '\u0458', 1219 | jukcy: '\u0454', 1220 | kappa: '\u03BA', 1221 | kappav: '\u03F0', 1222 | kcedil: '\u0137', 1223 | kcy: '\u043A', 1224 | kfr: '\uD835\uDD28', 1225 | kgreen: '\u0138', 1226 | khcy: '\u0445', 1227 | kjcy: '\u045C', 1228 | kopf: '\uD835\uDD5C', 1229 | kscr: '\uD835\uDCC0', 1230 | lAarr: '\u21DA', 1231 | lArr: '\u21D0', 1232 | lAtail: '\u291B', 1233 | lBarr: '\u290E', 1234 | lE: '\u2266', 1235 | lEg: '\u2A8B', 1236 | lHar: '\u2962', 1237 | lacute: '\u013A', 1238 | laemptyv: '\u29B4', 1239 | lagran: '\u2112', 1240 | lambda: '\u03BB', 1241 | lang: '\u27E8', 1242 | langd: '\u2991', 1243 | langle: '\u27E8', 1244 | lap: '\u2A85', 1245 | larr: '\u2190', 1246 | larrb: '\u21E4', 1247 | larrbfs: '\u291F', 1248 | larrfs: '\u291D', 1249 | larrhk: '\u21A9', 1250 | larrlp: '\u21AB', 1251 | larrpl: '\u2939', 1252 | larrsim: '\u2973', 1253 | larrtl: '\u21A2', 1254 | lat: '\u2AAB', 1255 | latail: '\u2919', 1256 | late: '\u2AAD', 1257 | lates: '\u2AAD\uFE00', 1258 | lbarr: '\u290C', 1259 | lbbrk: '\u2772', 1260 | lbrace: '\u007B', 1261 | lbrack: '\u005B', 1262 | lbrke: '\u298B', 1263 | lbrksld: '\u298F', 1264 | lbrkslu: '\u298D', 1265 | lcaron: '\u013E', 1266 | lcedil: '\u013C', 1267 | lceil: '\u2308', 1268 | lcub: '\u007B', 1269 | lcy: '\u043B', 1270 | ldca: '\u2936', 1271 | ldquo: '\u201C', 1272 | ldquor: '\u201E', 1273 | ldrdhar: '\u2967', 1274 | ldrushar: '\u294B', 1275 | ldsh: '\u21B2', 1276 | le: '\u2264', 1277 | leftarrow: '\u2190', 1278 | leftarrowtail: '\u21A2', 1279 | leftharpoondown: '\u21BD', 1280 | leftharpoonup: '\u21BC', 1281 | leftleftarrows: '\u21C7', 1282 | leftrightarrow: '\u2194', 1283 | leftrightarrows: '\u21C6', 1284 | leftrightharpoons: '\u21CB', 1285 | leftrightsquigarrow: '\u21AD', 1286 | leftthreetimes: '\u22CB', 1287 | leg: '\u22DA', 1288 | leq: '\u2264', 1289 | leqq: '\u2266', 1290 | leqslant: '\u2A7D', 1291 | les: '\u2A7D', 1292 | lescc: '\u2AA8', 1293 | lesdot: '\u2A7F', 1294 | lesdoto: '\u2A81', 1295 | lesdotor: '\u2A83', 1296 | lesg: '\u22DA\uFE00', 1297 | lesges: '\u2A93', 1298 | lessapprox: '\u2A85', 1299 | lessdot: '\u22D6', 1300 | lesseqgtr: '\u22DA', 1301 | lesseqqgtr: '\u2A8B', 1302 | lessgtr: '\u2276', 1303 | lesssim: '\u2272', 1304 | lfisht: '\u297C', 1305 | lfloor: '\u230A', 1306 | lfr: '\uD835\uDD29', 1307 | lg: '\u2276', 1308 | lgE: '\u2A91', 1309 | lhard: '\u21BD', 1310 | lharu: '\u21BC', 1311 | lharul: '\u296A', 1312 | lhblk: '\u2584', 1313 | ljcy: '\u0459', 1314 | ll: '\u226A', 1315 | llarr: '\u21C7', 1316 | llcorner: '\u231E', 1317 | llhard: '\u296B', 1318 | lltri: '\u25FA', 1319 | lmidot: '\u0140', 1320 | lmoust: '\u23B0', 1321 | lmoustache: '\u23B0', 1322 | lnE: '\u2268', 1323 | lnap: '\u2A89', 1324 | lnapprox: '\u2A89', 1325 | lne: '\u2A87', 1326 | lneq: '\u2A87', 1327 | lneqq: '\u2268', 1328 | lnsim: '\u22E6', 1329 | loang: '\u27EC', 1330 | loarr: '\u21FD', 1331 | lobrk: '\u27E6', 1332 | longleftarrow: '\u27F5', 1333 | longleftrightarrow: '\u27F7', 1334 | longmapsto: '\u27FC', 1335 | longrightarrow: '\u27F6', 1336 | looparrowleft: '\u21AB', 1337 | looparrowright: '\u21AC', 1338 | lopar: '\u2985', 1339 | lopf: '\uD835\uDD5D', 1340 | loplus: '\u2A2D', 1341 | lotimes: '\u2A34', 1342 | lowast: '\u2217', 1343 | lowbar: '\u005F', 1344 | loz: '\u25CA', 1345 | lozenge: '\u25CA', 1346 | lozf: '\u29EB', 1347 | lpar: '\u0028', 1348 | lparlt: '\u2993', 1349 | lrarr: '\u21C6', 1350 | lrcorner: '\u231F', 1351 | lrhar: '\u21CB', 1352 | lrhard: '\u296D', 1353 | lrm: '\u200E', 1354 | lrtri: '\u22BF', 1355 | lsaquo: '\u2039', 1356 | lscr: '\uD835\uDCC1', 1357 | lsh: '\u21B0', 1358 | lsim: '\u2272', 1359 | lsime: '\u2A8D', 1360 | lsimg: '\u2A8F', 1361 | lsqb: '\u005B', 1362 | lsquo: '\u2018', 1363 | lsquor: '\u201A', 1364 | lstrok: '\u0142', 1365 | ltcc: '\u2AA6', 1366 | ltcir: '\u2A79', 1367 | ltdot: '\u22D6', 1368 | lthree: '\u22CB', 1369 | ltimes: '\u22C9', 1370 | ltlarr: '\u2976', 1371 | ltquest: '\u2A7B', 1372 | ltrPar: '\u2996', 1373 | ltri: '\u25C3', 1374 | ltrie: '\u22B4', 1375 | ltrif: '\u25C2', 1376 | lurdshar: '\u294A', 1377 | luruhar: '\u2966', 1378 | lvertneqq: '\u2268\uFE00', 1379 | lvnE: '\u2268\uFE00', 1380 | mDDot: '\u223A', 1381 | male: '\u2642', 1382 | malt: '\u2720', 1383 | maltese: '\u2720', 1384 | map: '\u21A6', 1385 | mapsto: '\u21A6', 1386 | mapstodown: '\u21A7', 1387 | mapstoleft: '\u21A4', 1388 | mapstoup: '\u21A5', 1389 | marker: '\u25AE', 1390 | mcomma: '\u2A29', 1391 | mcy: '\u043C', 1392 | mdash: '\u2014', 1393 | measuredangle: '\u2221', 1394 | mfr: '\uD835\uDD2A', 1395 | mho: '\u2127', 1396 | mid: '\u2223', 1397 | midast: '\u002A', 1398 | midcir: '\u2AF0', 1399 | minus: '\u2212', 1400 | minusb: '\u229F', 1401 | minusd: '\u2238', 1402 | minusdu: '\u2A2A', 1403 | mlcp: '\u2ADB', 1404 | mldr: '\u2026', 1405 | mnplus: '\u2213', 1406 | models: '\u22A7', 1407 | mopf: '\uD835\uDD5E', 1408 | mp: '\u2213', 1409 | mscr: '\uD835\uDCC2', 1410 | mstpos: '\u223E', 1411 | mu: '\u03BC', 1412 | multimap: '\u22B8', 1413 | mumap: '\u22B8', 1414 | nGg: '\u22D9\u0338', 1415 | nGt: '\u226B\u20D2', 1416 | nGtv: '\u226B\u0338', 1417 | nLeftarrow: '\u21CD', 1418 | nLeftrightarrow: '\u21CE', 1419 | nLl: '\u22D8\u0338', 1420 | nLt: '\u226A\u20D2', 1421 | nLtv: '\u226A\u0338', 1422 | nRightarrow: '\u21CF', 1423 | nVDash: '\u22AF', 1424 | nVdash: '\u22AE', 1425 | nabla: '\u2207', 1426 | nacute: '\u0144', 1427 | nang: '\u2220\u20D2', 1428 | nap: '\u2249', 1429 | napE: '\u2A70\u0338', 1430 | napid: '\u224B\u0338', 1431 | napos: '\u0149', 1432 | napprox: '\u2249', 1433 | natur: '\u266E', 1434 | natural: '\u266E', 1435 | naturals: '\u2115', 1436 | nbump: '\u224E\u0338', 1437 | nbumpe: '\u224F\u0338', 1438 | ncap: '\u2A43', 1439 | ncaron: '\u0148', 1440 | ncedil: '\u0146', 1441 | ncong: '\u2247', 1442 | ncongdot: '\u2A6D\u0338', 1443 | ncup: '\u2A42', 1444 | ncy: '\u043D', 1445 | ndash: '\u2013', 1446 | ne: '\u2260', 1447 | neArr: '\u21D7', 1448 | nearhk: '\u2924', 1449 | nearr: '\u2197', 1450 | nearrow: '\u2197', 1451 | nedot: '\u2250\u0338', 1452 | nequiv: '\u2262', 1453 | nesear: '\u2928', 1454 | nesim: '\u2242\u0338', 1455 | nexist: '\u2204', 1456 | nexists: '\u2204', 1457 | nfr: '\uD835\uDD2B', 1458 | ngE: '\u2267\u0338', 1459 | nge: '\u2271', 1460 | ngeq: '\u2271', 1461 | ngeqq: '\u2267\u0338', 1462 | ngeqslant: '\u2A7E\u0338', 1463 | nges: '\u2A7E\u0338', 1464 | ngsim: '\u2275', 1465 | ngt: '\u226F', 1466 | ngtr: '\u226F', 1467 | nhArr: '\u21CE', 1468 | nharr: '\u21AE', 1469 | nhpar: '\u2AF2', 1470 | ni: '\u220B', 1471 | nis: '\u22FC', 1472 | nisd: '\u22FA', 1473 | niv: '\u220B', 1474 | njcy: '\u045A', 1475 | nlArr: '\u21CD', 1476 | nlE: '\u2266\u0338', 1477 | nlarr: '\u219A', 1478 | nldr: '\u2025', 1479 | nle: '\u2270', 1480 | nleftarrow: '\u219A', 1481 | nleftrightarrow: '\u21AE', 1482 | nleq: '\u2270', 1483 | nleqq: '\u2266\u0338', 1484 | nleqslant: '\u2A7D\u0338', 1485 | nles: '\u2A7D\u0338', 1486 | nless: '\u226E', 1487 | nlsim: '\u2274', 1488 | nlt: '\u226E', 1489 | nltri: '\u22EA', 1490 | nltrie: '\u22EC', 1491 | nmid: '\u2224', 1492 | nopf: '\uD835\uDD5F', 1493 | notin: '\u2209', 1494 | notinE: '\u22F9\u0338', 1495 | notindot: '\u22F5\u0338', 1496 | notinva: '\u2209', 1497 | notinvb: '\u22F7', 1498 | notinvc: '\u22F6', 1499 | notni: '\u220C', 1500 | notniva: '\u220C', 1501 | notnivb: '\u22FE', 1502 | notnivc: '\u22FD', 1503 | npar: '\u2226', 1504 | nparallel: '\u2226', 1505 | nparsl: '\u2AFD\u20E5', 1506 | npart: '\u2202\u0338', 1507 | npolint: '\u2A14', 1508 | npr: '\u2280', 1509 | nprcue: '\u22E0', 1510 | npre: '\u2AAF\u0338', 1511 | nprec: '\u2280', 1512 | npreceq: '\u2AAF\u0338', 1513 | nrArr: '\u21CF', 1514 | nrarr: '\u219B', 1515 | nrarrc: '\u2933\u0338', 1516 | nrarrw: '\u219D\u0338', 1517 | nrightarrow: '\u219B', 1518 | nrtri: '\u22EB', 1519 | nrtrie: '\u22ED', 1520 | nsc: '\u2281', 1521 | nsccue: '\u22E1', 1522 | nsce: '\u2AB0\u0338', 1523 | nscr: '\uD835\uDCC3', 1524 | nshortmid: '\u2224', 1525 | nshortparallel: '\u2226', 1526 | nsim: '\u2241', 1527 | nsime: '\u2244', 1528 | nsimeq: '\u2244', 1529 | nsmid: '\u2224', 1530 | nspar: '\u2226', 1531 | nsqsube: '\u22E2', 1532 | nsqsupe: '\u22E3', 1533 | nsub: '\u2284', 1534 | nsubE: '\u2AC5\u0338', 1535 | nsube: '\u2288', 1536 | nsubset: '\u2282\u20D2', 1537 | nsubseteq: '\u2288', 1538 | nsubseteqq: '\u2AC5\u0338', 1539 | nsucc: '\u2281', 1540 | nsucceq: '\u2AB0\u0338', 1541 | nsup: '\u2285', 1542 | nsupE: '\u2AC6\u0338', 1543 | nsupe: '\u2289', 1544 | nsupset: '\u2283\u20D2', 1545 | nsupseteq: '\u2289', 1546 | nsupseteqq: '\u2AC6\u0338', 1547 | ntgl: '\u2279', 1548 | ntlg: '\u2278', 1549 | ntriangleleft: '\u22EA', 1550 | ntrianglelefteq: '\u22EC', 1551 | ntriangleright: '\u22EB', 1552 | ntrianglerighteq: '\u22ED', 1553 | nu: '\u03BD', 1554 | num: '\u0023', 1555 | numero: '\u2116', 1556 | numsp: '\u2007', 1557 | nvDash: '\u22AD', 1558 | nvHarr: '\u2904', 1559 | nvap: '\u224D\u20D2', 1560 | nvdash: '\u22AC', 1561 | nvge: '\u2265\u20D2', 1562 | nvgt: '\u003E\u20D2', 1563 | nvinfin: '\u29DE', 1564 | nvlArr: '\u2902', 1565 | nvle: '\u2264\u20D2', 1566 | nvlt: '\u003C\u20D2', 1567 | nvltrie: '\u22B4\u20D2', 1568 | nvrArr: '\u2903', 1569 | nvrtrie: '\u22B5\u20D2', 1570 | nvsim: '\u223C\u20D2', 1571 | nwArr: '\u21D6', 1572 | nwarhk: '\u2923', 1573 | nwarr: '\u2196', 1574 | nwarrow: '\u2196', 1575 | nwnear: '\u2927', 1576 | oS: '\u24C8', 1577 | oast: '\u229B', 1578 | ocir: '\u229A', 1579 | ocy: '\u043E', 1580 | odash: '\u229D', 1581 | odblac: '\u0151', 1582 | odiv: '\u2A38', 1583 | odot: '\u2299', 1584 | odsold: '\u29BC', 1585 | oelig: '\u0153', 1586 | ofcir: '\u29BF', 1587 | ofr: '\uD835\uDD2C', 1588 | ogon: '\u02DB', 1589 | ogt: '\u29C1', 1590 | ohbar: '\u29B5', 1591 | ohm: '\u03A9', 1592 | oint: '\u222E', 1593 | olarr: '\u21BA', 1594 | olcir: '\u29BE', 1595 | olcross: '\u29BB', 1596 | oline: '\u203E', 1597 | olt: '\u29C0', 1598 | omacr: '\u014D', 1599 | omega: '\u03C9', 1600 | omicron: '\u03BF', 1601 | omid: '\u29B6', 1602 | ominus: '\u2296', 1603 | oopf: '\uD835\uDD60', 1604 | opar: '\u29B7', 1605 | operp: '\u29B9', 1606 | oplus: '\u2295', 1607 | or: '\u2228', 1608 | orarr: '\u21BB', 1609 | ord: '\u2A5D', 1610 | order: '\u2134', 1611 | orderof: '\u2134', 1612 | origof: '\u22B6', 1613 | oror: '\u2A56', 1614 | orslope: '\u2A57', 1615 | orv: '\u2A5B', 1616 | oscr: '\u2134', 1617 | osol: '\u2298', 1618 | otimes: '\u2297', 1619 | otimesas: '\u2A36', 1620 | ovbar: '\u233D', 1621 | par: '\u2225', 1622 | parallel: '\u2225', 1623 | parsim: '\u2AF3', 1624 | parsl: '\u2AFD', 1625 | part: '\u2202', 1626 | pcy: '\u043F', 1627 | percnt: '\u0025', 1628 | period: '\u002E', 1629 | permil: '\u2030', 1630 | perp: '\u22A5', 1631 | pertenk: '\u2031', 1632 | pfr: '\uD835\uDD2D', 1633 | phi: '\u03C6', 1634 | phiv: '\u03D5', 1635 | phmmat: '\u2133', 1636 | phone: '\u260E', 1637 | pi: '\u03C0', 1638 | pitchfork: '\u22D4', 1639 | piv: '\u03D6', 1640 | planck: '\u210F', 1641 | planckh: '\u210E', 1642 | plankv: '\u210F', 1643 | plus: '\u002B', 1644 | plusacir: '\u2A23', 1645 | plusb: '\u229E', 1646 | pluscir: '\u2A22', 1647 | plusdo: '\u2214', 1648 | plusdu: '\u2A25', 1649 | pluse: '\u2A72', 1650 | plussim: '\u2A26', 1651 | plustwo: '\u2A27', 1652 | pm: '\u00B1', 1653 | pointint: '\u2A15', 1654 | popf: '\uD835\uDD61', 1655 | pr: '\u227A', 1656 | prE: '\u2AB3', 1657 | prap: '\u2AB7', 1658 | prcue: '\u227C', 1659 | pre: '\u2AAF', 1660 | prec: '\u227A', 1661 | precapprox: '\u2AB7', 1662 | preccurlyeq: '\u227C', 1663 | preceq: '\u2AAF', 1664 | precnapprox: '\u2AB9', 1665 | precneqq: '\u2AB5', 1666 | precnsim: '\u22E8', 1667 | precsim: '\u227E', 1668 | prime: '\u2032', 1669 | primes: '\u2119', 1670 | prnE: '\u2AB5', 1671 | prnap: '\u2AB9', 1672 | prnsim: '\u22E8', 1673 | prod: '\u220F', 1674 | profalar: '\u232E', 1675 | profline: '\u2312', 1676 | profsurf: '\u2313', 1677 | prop: '\u221D', 1678 | propto: '\u221D', 1679 | prsim: '\u227E', 1680 | prurel: '\u22B0', 1681 | pscr: '\uD835\uDCC5', 1682 | psi: '\u03C8', 1683 | puncsp: '\u2008', 1684 | qfr: '\uD835\uDD2E', 1685 | qint: '\u2A0C', 1686 | qopf: '\uD835\uDD62', 1687 | qprime: '\u2057', 1688 | qscr: '\uD835\uDCC6', 1689 | quaternions: '\u210D', 1690 | quatint: '\u2A16', 1691 | quest: '\u003F', 1692 | questeq: '\u225F', 1693 | rAarr: '\u21DB', 1694 | rArr: '\u21D2', 1695 | rAtail: '\u291C', 1696 | rBarr: '\u290F', 1697 | rHar: '\u2964', 1698 | race: '\u223D\u0331', 1699 | racute: '\u0155', 1700 | radic: '\u221A', 1701 | raemptyv: '\u29B3', 1702 | rang: '\u27E9', 1703 | rangd: '\u2992', 1704 | range: '\u29A5', 1705 | rangle: '\u27E9', 1706 | rarr: '\u2192', 1707 | rarrap: '\u2975', 1708 | rarrb: '\u21E5', 1709 | rarrbfs: '\u2920', 1710 | rarrc: '\u2933', 1711 | rarrfs: '\u291E', 1712 | rarrhk: '\u21AA', 1713 | rarrlp: '\u21AC', 1714 | rarrpl: '\u2945', 1715 | rarrsim: '\u2974', 1716 | rarrtl: '\u21A3', 1717 | rarrw: '\u219D', 1718 | ratail: '\u291A', 1719 | ratio: '\u2236', 1720 | rationals: '\u211A', 1721 | rbarr: '\u290D', 1722 | rbbrk: '\u2773', 1723 | rbrace: '\u007D', 1724 | rbrack: '\u005D', 1725 | rbrke: '\u298C', 1726 | rbrksld: '\u298E', 1727 | rbrkslu: '\u2990', 1728 | rcaron: '\u0159', 1729 | rcedil: '\u0157', 1730 | rceil: '\u2309', 1731 | rcub: '\u007D', 1732 | rcy: '\u0440', 1733 | rdca: '\u2937', 1734 | rdldhar: '\u2969', 1735 | rdquo: '\u201D', 1736 | rdquor: '\u201D', 1737 | rdsh: '\u21B3', 1738 | real: '\u211C', 1739 | realine: '\u211B', 1740 | realpart: '\u211C', 1741 | reals: '\u211D', 1742 | rect: '\u25AD', 1743 | rfisht: '\u297D', 1744 | rfloor: '\u230B', 1745 | rfr: '\uD835\uDD2F', 1746 | rhard: '\u21C1', 1747 | rharu: '\u21C0', 1748 | rharul: '\u296C', 1749 | rho: '\u03C1', 1750 | rhov: '\u03F1', 1751 | rightarrow: '\u2192', 1752 | rightarrowtail: '\u21A3', 1753 | rightharpoondown: '\u21C1', 1754 | rightharpoonup: '\u21C0', 1755 | rightleftarrows: '\u21C4', 1756 | rightleftharpoons: '\u21CC', 1757 | rightrightarrows: '\u21C9', 1758 | rightsquigarrow: '\u219D', 1759 | rightthreetimes: '\u22CC', 1760 | ring: '\u02DA', 1761 | risingdotseq: '\u2253', 1762 | rlarr: '\u21C4', 1763 | rlhar: '\u21CC', 1764 | rlm: '\u200F', 1765 | rmoust: '\u23B1', 1766 | rmoustache: '\u23B1', 1767 | rnmid: '\u2AEE', 1768 | roang: '\u27ED', 1769 | roarr: '\u21FE', 1770 | robrk: '\u27E7', 1771 | ropar: '\u2986', 1772 | ropf: '\uD835\uDD63', 1773 | roplus: '\u2A2E', 1774 | rotimes: '\u2A35', 1775 | rpar: '\u0029', 1776 | rpargt: '\u2994', 1777 | rppolint: '\u2A12', 1778 | rrarr: '\u21C9', 1779 | rsaquo: '\u203A', 1780 | rscr: '\uD835\uDCC7', 1781 | rsh: '\u21B1', 1782 | rsqb: '\u005D', 1783 | rsquo: '\u2019', 1784 | rsquor: '\u2019', 1785 | rthree: '\u22CC', 1786 | rtimes: '\u22CA', 1787 | rtri: '\u25B9', 1788 | rtrie: '\u22B5', 1789 | rtrif: '\u25B8', 1790 | rtriltri: '\u29CE', 1791 | ruluhar: '\u2968', 1792 | rx: '\u211E', 1793 | sacute: '\u015B', 1794 | sbquo: '\u201A', 1795 | sc: '\u227B', 1796 | scE: '\u2AB4', 1797 | scap: '\u2AB8', 1798 | scaron: '\u0161', 1799 | sccue: '\u227D', 1800 | sce: '\u2AB0', 1801 | scedil: '\u015F', 1802 | scirc: '\u015D', 1803 | scnE: '\u2AB6', 1804 | scnap: '\u2ABA', 1805 | scnsim: '\u22E9', 1806 | scpolint: '\u2A13', 1807 | scsim: '\u227F', 1808 | scy: '\u0441', 1809 | sdot: '\u22C5', 1810 | sdotb: '\u22A1', 1811 | sdote: '\u2A66', 1812 | seArr: '\u21D8', 1813 | searhk: '\u2925', 1814 | searr: '\u2198', 1815 | searrow: '\u2198', 1816 | semi: '\u003B', 1817 | seswar: '\u2929', 1818 | setminus: '\u2216', 1819 | setmn: '\u2216', 1820 | sext: '\u2736', 1821 | sfr: '\uD835\uDD30', 1822 | sfrown: '\u2322', 1823 | sharp: '\u266F', 1824 | shchcy: '\u0449', 1825 | shcy: '\u0448', 1826 | shortmid: '\u2223', 1827 | shortparallel: '\u2225', 1828 | sigma: '\u03C3', 1829 | sigmaf: '\u03C2', 1830 | sigmav: '\u03C2', 1831 | sim: '\u223C', 1832 | simdot: '\u2A6A', 1833 | sime: '\u2243', 1834 | simeq: '\u2243', 1835 | simg: '\u2A9E', 1836 | simgE: '\u2AA0', 1837 | siml: '\u2A9D', 1838 | simlE: '\u2A9F', 1839 | simne: '\u2246', 1840 | simplus: '\u2A24', 1841 | simrarr: '\u2972', 1842 | slarr: '\u2190', 1843 | smallsetminus: '\u2216', 1844 | smashp: '\u2A33', 1845 | smeparsl: '\u29E4', 1846 | smid: '\u2223', 1847 | smile: '\u2323', 1848 | smt: '\u2AAA', 1849 | smte: '\u2AAC', 1850 | smtes: '\u2AAC\uFE00', 1851 | softcy: '\u044C', 1852 | sol: '\u002F', 1853 | solb: '\u29C4', 1854 | solbar: '\u233F', 1855 | sopf: '\uD835\uDD64', 1856 | spades: '\u2660', 1857 | spadesuit: '\u2660', 1858 | spar: '\u2225', 1859 | sqcap: '\u2293', 1860 | sqcaps: '\u2293\uFE00', 1861 | sqcup: '\u2294', 1862 | sqcups: '\u2294\uFE00', 1863 | sqsub: '\u228F', 1864 | sqsube: '\u2291', 1865 | sqsubset: '\u228F', 1866 | sqsubseteq: '\u2291', 1867 | sqsup: '\u2290', 1868 | sqsupe: '\u2292', 1869 | sqsupset: '\u2290', 1870 | sqsupseteq: '\u2292', 1871 | squ: '\u25A1', 1872 | square: '\u25A1', 1873 | squarf: '\u25AA', 1874 | squf: '\u25AA', 1875 | srarr: '\u2192', 1876 | sscr: '\uD835\uDCC8', 1877 | ssetmn: '\u2216', 1878 | ssmile: '\u2323', 1879 | sstarf: '\u22C6', 1880 | star: '\u2606', 1881 | starf: '\u2605', 1882 | straightepsilon: '\u03F5', 1883 | straightphi: '\u03D5', 1884 | strns: '\u00AF', 1885 | sub: '\u2282', 1886 | subE: '\u2AC5', 1887 | subdot: '\u2ABD', 1888 | sube: '\u2286', 1889 | subedot: '\u2AC3', 1890 | submult: '\u2AC1', 1891 | subnE: '\u2ACB', 1892 | subne: '\u228A', 1893 | subplus: '\u2ABF', 1894 | subrarr: '\u2979', 1895 | subset: '\u2282', 1896 | subseteq: '\u2286', 1897 | subseteqq: '\u2AC5', 1898 | subsetneq: '\u228A', 1899 | subsetneqq: '\u2ACB', 1900 | subsim: '\u2AC7', 1901 | subsub: '\u2AD5', 1902 | subsup: '\u2AD3', 1903 | succ: '\u227B', 1904 | succapprox: '\u2AB8', 1905 | succcurlyeq: '\u227D', 1906 | succeq: '\u2AB0', 1907 | succnapprox: '\u2ABA', 1908 | succneqq: '\u2AB6', 1909 | succnsim: '\u22E9', 1910 | succsim: '\u227F', 1911 | sum: '\u2211', 1912 | sung: '\u266A', 1913 | sup: '\u2283', 1914 | supE: '\u2AC6', 1915 | supdot: '\u2ABE', 1916 | supdsub: '\u2AD8', 1917 | supe: '\u2287', 1918 | supedot: '\u2AC4', 1919 | suphsol: '\u27C9', 1920 | suphsub: '\u2AD7', 1921 | suplarr: '\u297B', 1922 | supmult: '\u2AC2', 1923 | supnE: '\u2ACC', 1924 | supne: '\u228B', 1925 | supplus: '\u2AC0', 1926 | supset: '\u2283', 1927 | supseteq: '\u2287', 1928 | supseteqq: '\u2AC6', 1929 | supsetneq: '\u228B', 1930 | supsetneqq: '\u2ACC', 1931 | supsim: '\u2AC8', 1932 | supsub: '\u2AD4', 1933 | supsup: '\u2AD6', 1934 | swArr: '\u21D9', 1935 | swarhk: '\u2926', 1936 | swarr: '\u2199', 1937 | swarrow: '\u2199', 1938 | swnwar: '\u292A', 1939 | target: '\u2316', 1940 | tau: '\u03C4', 1941 | tbrk: '\u23B4', 1942 | tcaron: '\u0165', 1943 | tcedil: '\u0163', 1944 | tcy: '\u0442', 1945 | tdot: '\u20DB', 1946 | telrec: '\u2315', 1947 | tfr: '\uD835\uDD31', 1948 | there4: '\u2234', 1949 | therefore: '\u2234', 1950 | theta: '\u03B8', 1951 | thetasym: '\u03D1', 1952 | thetav: '\u03D1', 1953 | thickapprox: '\u2248', 1954 | thicksim: '\u223C', 1955 | thinsp: '\u2009', 1956 | thkap: '\u2248', 1957 | thksim: '\u223C', 1958 | tilde: '\u02DC', 1959 | timesb: '\u22A0', 1960 | timesbar: '\u2A31', 1961 | timesd: '\u2A30', 1962 | tint: '\u222D', 1963 | toea: '\u2928', 1964 | top: '\u22A4', 1965 | topbot: '\u2336', 1966 | topcir: '\u2AF1', 1967 | topf: '\uD835\uDD65', 1968 | topfork: '\u2ADA', 1969 | tosa: '\u2929', 1970 | tprime: '\u2034', 1971 | trade: '\u2122', 1972 | triangle: '\u25B5', 1973 | triangledown: '\u25BF', 1974 | triangleleft: '\u25C3', 1975 | trianglelefteq: '\u22B4', 1976 | triangleq: '\u225C', 1977 | triangleright: '\u25B9', 1978 | trianglerighteq: '\u22B5', 1979 | tridot: '\u25EC', 1980 | trie: '\u225C', 1981 | triminus: '\u2A3A', 1982 | triplus: '\u2A39', 1983 | trisb: '\u29CD', 1984 | tritime: '\u2A3B', 1985 | trpezium: '\u23E2', 1986 | tscr: '\uD835\uDCC9', 1987 | tscy: '\u0446', 1988 | tshcy: '\u045B', 1989 | tstrok: '\u0167', 1990 | twixt: '\u226C', 1991 | twoheadleftarrow: '\u219E', 1992 | twoheadrightarrow: '\u21A0', 1993 | uArr: '\u21D1', 1994 | uHar: '\u2963', 1995 | uarr: '\u2191', 1996 | ubrcy: '\u045E', 1997 | ubreve: '\u016D', 1998 | ucy: '\u0443', 1999 | udarr: '\u21C5', 2000 | udblac: '\u0171', 2001 | udhar: '\u296E', 2002 | ufisht: '\u297E', 2003 | ufr: '\uD835\uDD32', 2004 | uharl: '\u21BF', 2005 | uharr: '\u21BE', 2006 | uhblk: '\u2580', 2007 | ulcorn: '\u231C', 2008 | ulcorner: '\u231C', 2009 | ulcrop: '\u230F', 2010 | ultri: '\u25F8', 2011 | umacr: '\u016B', 2012 | uogon: '\u0173', 2013 | uopf: '\uD835\uDD66', 2014 | uparrow: '\u2191', 2015 | updownarrow: '\u2195', 2016 | upharpoonleft: '\u21BF', 2017 | upharpoonright: '\u21BE', 2018 | uplus: '\u228E', 2019 | upsi: '\u03C5', 2020 | upsih: '\u03D2', 2021 | upsilon: '\u03C5', 2022 | upuparrows: '\u21C8', 2023 | urcorn: '\u231D', 2024 | urcorner: '\u231D', 2025 | urcrop: '\u230E', 2026 | uring: '\u016F', 2027 | urtri: '\u25F9', 2028 | uscr: '\uD835\uDCCA', 2029 | utdot: '\u22F0', 2030 | utilde: '\u0169', 2031 | utri: '\u25B5', 2032 | utrif: '\u25B4', 2033 | uuarr: '\u21C8', 2034 | uwangle: '\u29A7', 2035 | vArr: '\u21D5', 2036 | vBar: '\u2AE8', 2037 | vBarv: '\u2AE9', 2038 | vDash: '\u22A8', 2039 | vangrt: '\u299C', 2040 | varepsilon: '\u03F5', 2041 | varkappa: '\u03F0', 2042 | varnothing: '\u2205', 2043 | varphi: '\u03D5', 2044 | varpi: '\u03D6', 2045 | varpropto: '\u221D', 2046 | varr: '\u2195', 2047 | varrho: '\u03F1', 2048 | varsigma: '\u03C2', 2049 | varsubsetneq: '\u228A\uFE00', 2050 | varsubsetneqq: '\u2ACB\uFE00', 2051 | varsupsetneq: '\u228B\uFE00', 2052 | varsupsetneqq: '\u2ACC\uFE00', 2053 | vartheta: '\u03D1', 2054 | vartriangleleft: '\u22B2', 2055 | vartriangleright: '\u22B3', 2056 | vcy: '\u0432', 2057 | vdash: '\u22A2', 2058 | vee: '\u2228', 2059 | veebar: '\u22BB', 2060 | veeeq: '\u225A', 2061 | vellip: '\u22EE', 2062 | verbar: '\u007C', 2063 | vert: '\u007C', 2064 | vfr: '\uD835\uDD33', 2065 | vltri: '\u22B2', 2066 | vnsub: '\u2282\u20D2', 2067 | vnsup: '\u2283\u20D2', 2068 | vopf: '\uD835\uDD67', 2069 | vprop: '\u221D', 2070 | vrtri: '\u22B3', 2071 | vscr: '\uD835\uDCCB', 2072 | vsubnE: '\u2ACB\uFE00', 2073 | vsubne: '\u228A\uFE00', 2074 | vsupnE: '\u2ACC\uFE00', 2075 | vsupne: '\u228B\uFE00', 2076 | vzigzag: '\u299A', 2077 | wcirc: '\u0175', 2078 | wedbar: '\u2A5F', 2079 | wedge: '\u2227', 2080 | wedgeq: '\u2259', 2081 | weierp: '\u2118', 2082 | wfr: '\uD835\uDD34', 2083 | wopf: '\uD835\uDD68', 2084 | wp: '\u2118', 2085 | wr: '\u2240', 2086 | wreath: '\u2240', 2087 | wscr: '\uD835\uDCCC', 2088 | xcap: '\u22C2', 2089 | xcirc: '\u25EF', 2090 | xcup: '\u22C3', 2091 | xdtri: '\u25BD', 2092 | xfr: '\uD835\uDD35', 2093 | xhArr: '\u27FA', 2094 | xharr: '\u27F7', 2095 | xi: '\u03BE', 2096 | xlArr: '\u27F8', 2097 | xlarr: '\u27F5', 2098 | xmap: '\u27FC', 2099 | xnis: '\u22FB', 2100 | xodot: '\u2A00', 2101 | xopf: '\uD835\uDD69', 2102 | xoplus: '\u2A01', 2103 | xotime: '\u2A02', 2104 | xrArr: '\u27F9', 2105 | xrarr: '\u27F6', 2106 | xscr: '\uD835\uDCCD', 2107 | xsqcup: '\u2A06', 2108 | xuplus: '\u2A04', 2109 | xutri: '\u25B3', 2110 | xvee: '\u22C1', 2111 | xwedge: '\u22C0', 2112 | yacy: '\u044F', 2113 | ycirc: '\u0177', 2114 | ycy: '\u044B', 2115 | yfr: '\uD835\uDD36', 2116 | yicy: '\u0457', 2117 | yopf: '\uD835\uDD6A', 2118 | yscr: '\uD835\uDCCE', 2119 | yucy: '\u044E', 2120 | zacute: '\u017A', 2121 | zcaron: '\u017E', 2122 | zcy: '\u0437', 2123 | zdot: '\u017C', 2124 | zeetrf: '\u2128', 2125 | zeta: '\u03B6', 2126 | zfr: '\uD835\uDD37', 2127 | zhcy: '\u0436', 2128 | zigrarr: '\u21DD', 2129 | zopf: '\uD835\uDD6B', 2130 | zscr: '\uD835\uDCCF', 2131 | zwj: '\u200D', 2132 | zwnj: '\u200C', 2133 | }; 2134 | 2135 | /** 2136 | * entity is a map from HTML entity names to their values. 2137 | * 2138 | * @see https://html.spec.whatwg.org/multipage/named-characters.html 2139 | */ 2140 | export const fullNamedEntities: Record = {}; 2141 | 2142 | let isBuild = false; 2143 | export function buildFullNamedEntities() { 2144 | if (isBuild) { 2145 | return; 2146 | } 2147 | 2148 | Object.keys(namedEntitiesWithoutSemicolon).forEach((entity) => { 2149 | fullNamedEntities[entity + ';'] = namedEntitiesWithoutSemicolon[entity]; 2150 | fullNamedEntities[entity] = namedEntitiesWithoutSemicolon[entity]; 2151 | }); 2152 | 2153 | Object.keys(namedEntities).forEach((entity) => { 2154 | fullNamedEntities[entity + ';'] = namedEntities[entity]; 2155 | }); 2156 | 2157 | isBuild = true; 2158 | } 2159 | -------------------------------------------------------------------------------- /src/html/escape.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/quotes */ 2 | import { Replacer } from '../internal/replacer'; 3 | import { fromCodePoint, getCodePointAt, numericUnicodeMap } from '../internal/codepoints'; 4 | import { Dictionary } from '../other/types'; 5 | import { buildFullNamedEntities, fullNamedEntities } from './entity'; 6 | 7 | const escapeReplacer = /*#__PURE__*/ new Replacer({ 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | "'": ''', 12 | '"': '"', 13 | }); 14 | 15 | const unescapeReplacer = /*#__PURE__*/ new Replacer({ 16 | '&': '&', 17 | '&': '&', 18 | '<': '<', 19 | '<': '<', 20 | '>': '>', 21 | '>': '>', 22 | ''': "'", 23 | ''': "'", 24 | '"': '"', 25 | '"': '"', 26 | }); 27 | 28 | const namedEntities: Record = { 29 | 'amp;': '&', 30 | 'lt;': '<', 31 | 'gt;': '>', 32 | 'quot;': '"', 33 | 'apos;': `'`, 34 | }; 35 | 36 | /** 37 | * Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'` 38 | * @param {string} input 39 | */ 40 | export function escape(input: string): string { 41 | return escapeReplacer.replace(input); 42 | } 43 | 44 | /** 45 | * Unescape HTML entities such as `&`, `<`, `>`, `"`, and `'` 46 | * @param {string} input 47 | */ 48 | export function unescape(input: string): string { 49 | return unescapeReplacer.replace(input); 50 | } 51 | 52 | const outOfBoundsChar = /*#__PURE__*/ String.fromCharCode(65533); 53 | 54 | const ENCODE_REGEX = 55 | /(?:[<>'"&\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g; 56 | 57 | export function encodeHTMLEntities(input: string): string { 58 | if (input == null) { 59 | return ''; 60 | } 61 | 62 | return input.replace(ENCODE_REGEX, (entity) => { 63 | const code = entity.length > 1 ? getCodePointAt(entity, 0) : entity.charCodeAt(0); 64 | return '&#' + String(code) + ';'; 65 | }); 66 | } 67 | 68 | const DECODE_REGEX = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+);?/g; 69 | 70 | function decodeString(input: string, entities: Record): string { 71 | if (typeof input !== 'string' || !input) { 72 | return ''; 73 | } 74 | 75 | return input.replace(DECODE_REGEX, (entity) => { 76 | if (entity[1] === '#') { 77 | // We need to have at least "&#.". 78 | if (entity.length <= 3) { 79 | return entity; 80 | } 81 | 82 | const secondChar = entity.charAt(2); 83 | const code = 84 | secondChar === 'x' || secondChar === 'X' 85 | ? parseInt(entity.substr(3).toLowerCase(), 16) 86 | : parseInt(entity.substr(2)); 87 | 88 | if (code >= 0x10ffff) { 89 | return outOfBoundsChar; 90 | } 91 | 92 | if (code > 65535) { 93 | return fromCodePoint(code); 94 | } 95 | 96 | return String.fromCharCode(numericUnicodeMap[code] || code); 97 | } 98 | 99 | return entities[entity.slice(1)] || entity; 100 | }); 101 | } 102 | 103 | export function decodeHTMLEntitiesDeep(input: T): T { 104 | if (typeof input === 'string') { 105 | return decodeHTMLEntities(input) as unknown as T; 106 | } 107 | 108 | if (typeof input === 'object') { 109 | const correctType = Object.prototype.toString.apply(input); 110 | 111 | if (correctType === '[object Array]') { 112 | return (input as unknown[]).map((item) => { 113 | return decodeHTMLEntitiesDeep(item); 114 | }) as T; 115 | } 116 | 117 | if (correctType === '[object Object]') { 118 | const response: Dictionary = {}; 119 | Object.keys(input as Dictionary).forEach((item) => { 120 | response[decodeHTMLEntities(item)] = decodeHTMLEntitiesDeep( 121 | (input as Dictionary)[item], 122 | ); 123 | }); 124 | 125 | return response as unknown as T; 126 | } 127 | } 128 | 129 | return input; 130 | } 131 | 132 | /** 133 | * `decodeHTMLEntities` декодирует зарезервированные HTML-сущности. 134 | * 135 | * Если нужна возможность декодировать все сущности, используйте 136 | * {@link decodeHTMLFullEntities} 137 | * 138 | * @param input Текст который необходимо декодировать 139 | * @param entities Кастомный словарь сущностей `{'lt;': '<'}` 140 | */ 141 | export function decodeHTMLEntities(input: string, entities = namedEntities): string { 142 | return decodeString(input, entities); 143 | } 144 | 145 | /** 146 | * `decodeHTMLFullEntities` декодирует все HTML-сущности. 147 | * 148 | * Если вам нужно декодировать не все сущности, используйте 149 | * {@link decodeHTMLEntities} и кастомный словарь. 150 | * 151 | * @param input Текст который необходимо декодировать 152 | */ 153 | export function decodeHTMLFullEntities(input: string): string { 154 | buildFullNamedEntities(); 155 | 156 | return decodeString(input, fullNamedEntities); 157 | } 158 | -------------------------------------------------------------------------------- /src/html/index.ts: -------------------------------------------------------------------------------- 1 | export * from './escape'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './animation'; 2 | export type * from './animation'; 3 | 4 | export * from './array'; 5 | export type * from './array'; 6 | 7 | export * from './async'; 8 | export type * from './async'; 9 | 10 | export * from './datetime'; 11 | export type * from './datetime'; 12 | 13 | export * from './device'; 14 | export type * from './device'; 15 | 16 | export * from './html'; 17 | export type * from './html'; 18 | 19 | export * from './other'; 20 | export type * from './other'; 21 | 22 | export * from './text'; 23 | export type * from './text'; 24 | 25 | export * from './typecheck'; 26 | export type * from './typecheck'; 27 | -------------------------------------------------------------------------------- /src/internal/codepoints.ts: -------------------------------------------------------------------------------- 1 | export const numericUnicodeMap: Record = { 2 | 0: 65533, 3 | 128: 8364, 4 | 130: 8218, 5 | 131: 402, 6 | 132: 8222, 7 | 133: 8230, 8 | 134: 8224, 9 | 135: 8225, 10 | 136: 710, 11 | 137: 8240, 12 | 138: 352, 13 | 139: 8249, 14 | 140: 338, 15 | 142: 381, 16 | 145: 8216, 17 | 146: 8217, 18 | 147: 8220, 19 | 148: 8221, 20 | 149: 8226, 21 | 150: 8211, 22 | 151: 8212, 23 | 152: 732, 24 | 153: 8482, 25 | 154: 353, 26 | 155: 8250, 27 | 156: 339, 28 | 158: 382, 29 | 159: 376, 30 | }; 31 | 32 | export const highSurrogateFrom = 0xd800; 33 | export const highSurrogateTo = 0xdbff; 34 | 35 | export const fromCodePoint = /*#__PURE__*/ (() => 36 | String.fromCodePoint || 37 | function (astralCodePoint: number) { 38 | return String.fromCharCode( 39 | Math.floor((astralCodePoint - 0x10000) / 0x400) + 0xd800, 40 | ((astralCodePoint - 0x10000) % 0x400) + 0xdc00, 41 | ); 42 | })(); 43 | 44 | const codePointAtNative = /*#__PURE__*/ (() => 45 | // eslint-disable-next-line @typescript-eslint/unbound-method 46 | String.prototype.codePointAt as typeof String.prototype.codePointAt | undefined)(); 47 | 48 | export const getCodePointAt = /*#__PURE__*/ (() => 49 | codePointAtNative 50 | ? function (input: string, position: number) { 51 | return input.codePointAt(position); 52 | } 53 | : function (input: string, position: number) { 54 | return ( 55 | (input.charCodeAt(position) - 0xd800) * 0x400 + 56 | input.charCodeAt(position + 1) - 57 | 0xdc00 + 58 | 0x10000 59 | ); 60 | })(); 61 | -------------------------------------------------------------------------------- /src/internal/replacer.ts: -------------------------------------------------------------------------------- 1 | import { escapeRegExp } from '../other/regexp'; 2 | 3 | export class Replacer { 4 | private regexp: RegExp | undefined; 5 | private readonly map: Record; 6 | 7 | constructor(map: Record) { 8 | this.map = map; 9 | } 10 | 11 | private build() { 12 | if (this.regexp) { 13 | return; 14 | } 15 | 16 | const groups = Object.keys(this.map) 17 | .map(escapeRegExp) 18 | .sort((a, b) => b.length - a.length); 19 | const pattern = `(?:${groups.join('|')})`; 20 | 21 | this.regexp = new RegExp(pattern, 'g'); 22 | } 23 | 24 | replace(string: string) { 25 | if (!string) { 26 | return ''; 27 | } 28 | 29 | this.build(); 30 | 31 | return string.replace(this.regexp!, (substring) => this.map[substring]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/internal/uniqueArray.ts: -------------------------------------------------------------------------------- 1 | export function uniqueArrayFallback(array: readonly T[]): T[] { 2 | if (!Array.isArray(array) || !array.length) { 3 | return []; 4 | } 5 | 6 | const map: Record = {}; 7 | const newArr: T[] = []; 8 | 9 | for (let i = 0; i < array.length; i++) { 10 | const item = array[i]; 11 | const key = String(item) + typeof item; 12 | 13 | if (!map[key]) { 14 | newArr.push(item); 15 | map[key] = true; 16 | } 17 | } 18 | 19 | return newArr; 20 | } 21 | -------------------------------------------------------------------------------- /src/other/clipboard.ts: -------------------------------------------------------------------------------- 1 | function copyWithNavigator(text: string): Promise { 2 | return navigator.clipboard.writeText(text).then(() => true); 3 | } 4 | 5 | function copyWithFakeElement(text: string): Promise { 6 | return new Promise((resolve, reject) => { 7 | const textareaEl = document.createElement('textarea'); 8 | const range = document.createRange(); 9 | 10 | textareaEl.value = text; 11 | textareaEl.style.position = 'fixed'; // Avoid scrolling to bottom 12 | textareaEl.contentEditable = 'true'; 13 | 14 | document.body.appendChild(textareaEl); 15 | 16 | textareaEl.focus(); 17 | textareaEl.select(); 18 | 19 | range.selectNodeContents(textareaEl); 20 | 21 | const selection = window.getSelection(); 22 | if (selection) { 23 | selection.removeAllRanges(); 24 | selection.addRange(range); 25 | } 26 | 27 | textareaEl.setSelectionRange(0, 999999); 28 | 29 | try { 30 | const successful = document.execCommand('copy'); 31 | if (successful) { 32 | resolve(true); 33 | } else { 34 | reject(new Error('copy failed')); 35 | } 36 | } catch (error) { 37 | reject(error); 38 | } 39 | 40 | if (selection) { 41 | selection.removeAllRanges(); 42 | } 43 | 44 | document.body.removeChild(textareaEl); 45 | }); 46 | } 47 | 48 | export function copyTextToClipboard(text: string): Promise { 49 | if (navigator.clipboard) { 50 | return copyWithNavigator(text).catch(() => copyWithFakeElement(text)); 51 | } else { 52 | return copyWithFakeElement(text); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/other/common.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@jest/globals'; 2 | import { isNumeric } from './common'; 3 | 4 | test('isNumeric function', () => { 5 | expect(isNumeric(1)).toEqual(true); 6 | 7 | expect(isNumeric('1')).toEqual(true); 8 | expect(isNumeric('+1')).toEqual(true); 9 | expect(isNumeric('-1')).toEqual(true); 10 | 11 | expect(isNumeric('1.5')).toEqual(true); 12 | expect(isNumeric('+1.5')).toEqual(true); 13 | expect(isNumeric('-1.5')).toEqual(true); 14 | 15 | expect(isNumeric([1])).toEqual(false); 16 | expect(isNumeric('1 false')).toEqual(false); 17 | 18 | expect(isNumeric(parseInt('false'))).toEqual(false); 19 | }); 20 | -------------------------------------------------------------------------------- /src/other/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if value is a number (excluding NaN), or is a numeric string. 3 | * 4 | * NOTE: It treats all Number-coercible strings as numeric (e.g. `'0x123'`, `'123e-1'`), 5 | * so for number-type values it's better to just use `Number.isFinite()`. 6 | */ 7 | export function isNumeric(value: any): boolean { 8 | return ( 9 | !isNaN(parseFloat(value)) && 10 | isFinite(value) && 11 | // Handle `[1]` being serialized and parsed as `1` 12 | !Array.isArray(value) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/other/cookie.ts: -------------------------------------------------------------------------------- 1 | export function getCookie(name: string): string | undefined { 2 | let matches = document.cookie.match( 3 | new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/+^])/g, '\\$1') + '=([^;]*)'), 4 | ); 5 | return matches ? decodeURIComponent(matches[1]) : undefined; 6 | } 7 | 8 | let isCookieEnabledCache: boolean | null = null; 9 | 10 | export const isCookieEnabled = () => { 11 | if (isCookieEnabledCache === null) { 12 | try { 13 | document.cookie = 'cookietest=1'; 14 | 15 | isCookieEnabledCache = document.cookie.includes('cookietest='); 16 | 17 | document.cookie = 'cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT'; 18 | } catch (e) { 19 | isCookieEnabledCache = false; 20 | } 21 | } 22 | 23 | return isCookieEnabledCache; 24 | }; 25 | -------------------------------------------------------------------------------- /src/other/detections.ts: -------------------------------------------------------------------------------- 1 | import { noop } from './functions'; 2 | import { canUseDOM, canUseEventListeners } from './dom'; 3 | 4 | export const isPassiveEventsSupported = /*#__PURE__*/ (() => { 5 | let isSupported = false; 6 | 7 | if (canUseEventListeners) { 8 | try { 9 | const options = Object.defineProperty({}, 'passive', { 10 | get() { 11 | isSupported = true; 12 | }, 13 | }); 14 | 15 | window.addEventListener('test', noop, options); 16 | window.removeEventListener('test', noop, options); 17 | } catch (e) {} 18 | } 19 | 20 | return isSupported; 21 | })(); 22 | 23 | function detectSmoothScrollSupport() { 24 | if (!canUseDOM) { 25 | return false; 26 | } 27 | 28 | let isSupported = false; 29 | try { 30 | const div = document.createElement('div'); 31 | div.scrollTo({ 32 | top: 0, 33 | get behavior(): ScrollBehavior { 34 | isSupported = true; 35 | return 'smooth'; 36 | }, 37 | }); 38 | } catch (e) {} 39 | return isSupported; 40 | } 41 | 42 | export const isSmoothScrollSupported = /*#__PURE__*/ detectSmoothScrollSupport(); 43 | -------------------------------------------------------------------------------- /src/other/dom.ts: -------------------------------------------------------------------------------- 1 | export const canUseDOM = /*#__PURE__*/ (() => 2 | !!(typeof window !== 'undefined' && window.document && window.document.createElement))(); 3 | 4 | export const canUseEventListeners: boolean = /*#__PURE__*/ (() => 5 | canUseDOM && !!window.addEventListener)(); 6 | 7 | export function onDOMLoaded(callback: (...args: any[]) => any) { 8 | if (document.readyState !== 'loading') { 9 | callback(); 10 | } else { 11 | document.addEventListener('DOMContentLoaded', callback); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/other/equal.ts: -------------------------------------------------------------------------------- 1 | import { isObjectLike } from '../typecheck/type_checkers'; 2 | 3 | export function isEqual(value: any, other: any): boolean { 4 | if (value === other) { 5 | return true; 6 | } 7 | 8 | if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) { 9 | return value !== value && other !== other; 10 | } 11 | 12 | if (isObjectLike(value) && isObjectLike(other)) { 13 | if (Object.keys(value).length !== Object.keys(other).length) { 14 | return false; 15 | } 16 | 17 | for (const prop in value) { 18 | if (value.hasOwnProperty(prop) && other.hasOwnProperty(prop)) { 19 | if (!isEqual(value[prop], other[prop])) { 20 | return false; 21 | } 22 | } else { 23 | return false; 24 | } 25 | } 26 | 27 | return true; 28 | } 29 | 30 | return false; 31 | } 32 | -------------------------------------------------------------------------------- /src/other/functions.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, jest, test, describe } from '@jest/globals'; 2 | import { once } from './functions'; 3 | 4 | describe('once', () => { 5 | test('should be called once', () => { 6 | const fn = jest.fn(); 7 | const fnOnce = once(fn); 8 | 9 | fnOnce(); 10 | fnOnce(); 11 | fnOnce(); 12 | 13 | expect(fn).toBeCalledTimes(1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/other/functions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Функция, которая ничего не делает 3 | */ 4 | export const noop = () => { 5 | // Совсем ничего не делает 6 | }; 7 | 8 | /** 9 | * Создает функцию, которая вызовет `fn` только один раз. Последующие вызовы 10 | * будут проигнорированы. 11 | * 12 | * @example 13 | * ```ts 14 | * import {once} from '@vkontakte/vkjs'; 15 | * 16 | * let counter = 0; 17 | * const onceFn = once(() => counter++); 18 | * 19 | * onceFn(); 20 | * onceFn(); 21 | * onceFn(); 22 | * 23 | * console.log(counter); // 1 24 | * ``` 25 | * 26 | * @param fn Функция, которую необходимо вызвать только один раз 27 | */ 28 | export function once any>(fn: T) { 29 | // TODO: once должна кэшировать данные, но она это не делает 30 | let called = false; 31 | return function (...args) { 32 | if (called) { 33 | return; 34 | } 35 | 36 | called = true; 37 | return fn.apply(this, args); 38 | } as T; 39 | } 40 | -------------------------------------------------------------------------------- /src/other/getOffsetRect.ts: -------------------------------------------------------------------------------- 1 | interface Bounds { 2 | top: number; 3 | left: number; 4 | width: number; 5 | height: number; 6 | } 7 | 8 | export function getOffsetRect(el: HTMLElement | SVGElement | Text | null): Bounds { 9 | const isElement = el instanceof HTMLElement || el instanceof SVGElement; 10 | 11 | if (typeof window === 'undefined' || !isElement) { 12 | return { 13 | top: 0, 14 | left: 0, 15 | width: 0, 16 | height: 0, 17 | }; 18 | } 19 | 20 | const box = el.getBoundingClientRect(); 21 | const body = document.body; 22 | const doc = document.documentElement; 23 | const scrollTop = window.pageYOffset || doc.scrollTop || body.scrollTop; 24 | const scrollLeft = window.pageXOffset || doc.scrollLeft || body.scrollLeft; 25 | const clientTop = doc.clientTop || body.clientTop || 0; 26 | const clientLeft = doc.clientLeft || body.clientLeft || 0; 27 | 28 | return { 29 | top: Math.round(box.top + scrollTop - clientTop), 30 | left: Math.round(box.left + scrollLeft - clientLeft), 31 | width: Math.round(box.width), 32 | height: Math.round(box.height), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/other/getPhotoSize.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { getPhotoSize, PhotoSizeLike } from './getPhotoSize'; 3 | 4 | describe('getPhotoSize', () => { 5 | it('recognizes an invalid or empty array', () => { 6 | expect(getPhotoSize(1 as any, 1)).toBeNull(); 7 | expect(getPhotoSize(null as any, 1)).toBeNull(); 8 | expect(getPhotoSize({} as any, 1)).toBeNull(); 9 | expect(getPhotoSize([], 1)).toBeNull(); 10 | }); 11 | 12 | it('handles src/url differences', () => { 13 | const url = 'https://vk.com'; 14 | 15 | const sizes: PhotoSizeLike[] = [ 16 | { width: 200, height: 200, src: url }, 17 | { width: 400, height: 400, url: url }, 18 | { width: 600, height: 600 }, 19 | ]; 20 | 21 | expect(getPhotoSize(sizes, 200)).toEqual({ width: 200, height: 200, url }); 22 | expect(getPhotoSize(sizes, 400)).toEqual({ width: 400, height: 400, url }); 23 | expect(getPhotoSize(sizes, 600)).toEqual({ width: 600, height: 600, url: '' }); 24 | }); 25 | 26 | it('returns the minimum achievable image width', () => { 27 | const photoSizes: PhotoSizeLike[] = [ 28 | { 29 | width: 550, 30 | height: 550, 31 | }, 32 | { 33 | width: 750, 34 | height: 750, 35 | }, 36 | { 37 | width: 50, 38 | height: 50, 39 | }, 40 | ]; 41 | 42 | expect(getPhotoSize(photoSizes, 500)).toEqual({ 43 | width: 550, 44 | height: 550, 45 | url: '', 46 | }); 47 | }); 48 | 49 | it('returns the maximum achievable image width', () => { 50 | const photoSizes: PhotoSizeLike[] = [ 51 | { 52 | width: 450, 53 | height: 550, 54 | }, 55 | { 56 | width: 350, 57 | height: 750, 58 | }, 59 | { 60 | width: 50, 61 | height: 50, 62 | }, 63 | { 64 | width: 240, 65 | height: 360, 66 | }, 67 | ]; 68 | 69 | expect(getPhotoSize(photoSizes, 500)).toEqual({ 70 | width: 450, 71 | height: 550, 72 | url: '', 73 | }); 74 | }); 75 | 76 | it('returns the minimum achievable image by width and height', () => { 77 | const photoSizes: PhotoSizeLike[] = [ 78 | { 79 | width: 550, 80 | height: 550, 81 | }, 82 | { 83 | width: 750, 84 | height: 750, 85 | }, 86 | { 87 | width: 50, 88 | height: 50, 89 | }, 90 | ]; 91 | 92 | expect(getPhotoSize(photoSizes, 500, 600)).toEqual({ 93 | width: 750, 94 | height: 750, 95 | url: '', 96 | }); 97 | }); 98 | 99 | it('returns the maximum achievable image by width and height', () => { 100 | const photoSizes: PhotoSizeLike[] = [ 101 | { 102 | width: 350, 103 | height: 250, 104 | }, 105 | { 106 | width: 450, 107 | height: 550, 108 | }, 109 | { 110 | width: 50, 111 | height: 50, 112 | }, 113 | ]; 114 | 115 | expect(getPhotoSize(photoSizes, 500, 600)).toEqual({ 116 | width: 450, 117 | height: 550, 118 | url: '', 119 | }); 120 | }); 121 | 122 | it('returns the maximum achievable image in height among the same width', () => { 123 | const photoSizes: PhotoSizeLike[] = [ 124 | { 125 | width: 350, 126 | height: 250, 127 | }, 128 | { 129 | width: 350, 130 | height: 550, 131 | }, 132 | { 133 | width: 350, 134 | height: 300, 135 | }, 136 | ]; 137 | 138 | expect(getPhotoSize(photoSizes, 500, 600)).toEqual({ 139 | width: 350, 140 | height: 550, 141 | url: '', 142 | }); 143 | }); 144 | 145 | it('returns the maximum size (Infinity hack)', () => { 146 | const photoSizes: PhotoSizeLike[] = [ 147 | { 148 | width: 550, 149 | height: 550, 150 | }, 151 | { 152 | width: 750, 153 | height: 750, 154 | }, 155 | ]; 156 | 157 | expect(getPhotoSize(photoSizes, Infinity)).toEqual({ 158 | width: 750, 159 | height: 750, 160 | url: '', 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /src/other/getPhotoSize.ts: -------------------------------------------------------------------------------- 1 | export interface PhotoSizeLike { 2 | width: number; 3 | height: number; 4 | url?: string; 5 | src?: string; 6 | } 7 | 8 | export interface PhotoSize { 9 | url: string; 10 | width: number; 11 | height: number; 12 | } 13 | 14 | function prepareSize(size: PhotoSizeLike): PhotoSize { 15 | return { 16 | url: size.url || size.src || '', 17 | width: size.width, 18 | height: size.height, 19 | }; 20 | } 21 | 22 | function computeSize( 23 | sizes: PhotoSizeLike[], 24 | minWidth: number, 25 | minHeight: number | null = null, 26 | ): PhotoSizeLike { 27 | // Do nothing if sizes contains only 1 item 28 | if (sizes.length === 1) { 29 | return sizes[0]; 30 | } 31 | 32 | // Sorting in ascending order 33 | const sorted = [...sizes].sort((a, b) => { 34 | if (a.width < b.width) { 35 | return -1; 36 | } 37 | 38 | if (a.width === b.width) { 39 | return a.height > b.height ? 1 : -1; 40 | } 41 | 42 | return 1; 43 | }); 44 | 45 | const matchesByWidth = sorted.filter((size) => size.width >= minWidth); 46 | if (!matchesByWidth.length) { 47 | // Biggest size 48 | return sorted[sorted.length - 1]; 49 | } 50 | 51 | if (!minHeight) { 52 | return matchesByWidth[0]; 53 | } 54 | 55 | // Searching by height 56 | for (let i = 0; i < matchesByWidth.length; i++) { 57 | const size = matchesByWidth[i]; 58 | if (size.height >= minHeight) { 59 | return size; 60 | } 61 | } 62 | 63 | // Sorting by height in ascending order 64 | const sortedByHeight = matchesByWidth.sort((a, b) => (a.height > b.height ? 1 : -1)); 65 | 66 | return sortedByHeight[sortedByHeight.length - 1]; 67 | } 68 | 69 | /** 70 | * Searches for the smallest (?) suitable image from the sizes array. 71 | * 72 | * Or more precise, it returns: 73 | * – if no suitable sizes (>= minWidth И >= minHeight): the biggest from all sizes; 74 | * – there are any suitable (>= minWidth И >= minHeight): the smallest of the matching sizes; 75 | * 76 | * Returns null only in case of an empty/invalid array. The returned size can be smaller than minWidth or minHeight. 77 | * 78 | * The function doesn't take into about the retina screen (window.devicePixelRatio), so you have to calculate the right width/height from the outside to support it. 79 | * 80 | * WARN: 81 | * - does not reckon for letter-sizes (PhotosPhotoSizesType). 82 | * - does not know how to search for the "nearest" size, or the maximum image (there is a hack with the `Infinity` pass). 83 | * 84 | * @example 85 | * getPhotoSize([{ width: 1, height: 1 }, { width: 3, height: 3 }], 1) // => 1,1 86 | * getPhotoSize([{ width: 1, height: 1 }, { width: 3, height: 3 }], 2) // => 3,3 87 | * getPhotoSize([{ width: 1, height: 1 }, { width: 3, height: 3 }], 4) // => 3,3 88 | * 89 | * See more examples in tests 90 | */ 91 | export function getPhotoSize( 92 | sizes: PhotoSizeLike[], 93 | minWidth: number, 94 | minHeight: number | null = null, 95 | ): PhotoSize | null { 96 | if (!Array.isArray(sizes) || !sizes.length) { 97 | return null; 98 | } 99 | 100 | const size = computeSize(sizes, minWidth, minHeight); 101 | 102 | return prepareSize(size); 103 | } 104 | -------------------------------------------------------------------------------- /src/other/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * classNames 3 | */ 4 | export { clsx as classNames } from 'clsx'; 5 | 6 | /** 7 | * clipboard 8 | */ 9 | export { copyTextToClipboard } from './clipboard'; 10 | 11 | /** 12 | * common 13 | */ 14 | export { isNumeric } from './common'; 15 | 16 | /** 17 | * detections 18 | */ 19 | export { isPassiveEventsSupported, isSmoothScrollSupported } from './detections'; 20 | 21 | export { isEqual } from './equal'; 22 | 23 | export { noop, once } from './functions'; 24 | 25 | export { getCookie, isCookieEnabled } from './cookie'; 26 | 27 | /** 28 | * OffsetRect 29 | */ 30 | export { getOffsetRect } from './getOffsetRect'; 31 | 32 | export { getPhotoSize } from './getPhotoSize'; 33 | export type { PhotoSizeLike, PhotoSize } from './getPhotoSize'; 34 | 35 | /** 36 | * objects 37 | */ 38 | export { deleteObjectKeys } from './objects'; 39 | 40 | /** 41 | * querystring 42 | */ 43 | export { querystring } from './querystring'; 44 | 45 | /** 46 | * random 47 | */ 48 | export { getRandomInt, getRandomString } from './random'; 49 | 50 | export { hasReactNode, isPrimitiveReactNode } from './react_utils'; 51 | 52 | export type { Dictionary, AnyFunction, SupportEvent, TimeoutHandle, Writeable } from './types'; 53 | 54 | export { escapeRegExp } from './regexp'; 55 | 56 | export { localStorage, sessionStorage } from './storage'; 57 | 58 | export { canUseDOM, canUseEventListeners, onDOMLoaded } from './dom'; 59 | -------------------------------------------------------------------------------- /src/other/numbers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Добавляет к числу 0 в начале, если число меньше 10 3 | */ 4 | export function leadingZero(number: number): string { 5 | if (number >= 10) { 6 | return String(number); 7 | } else { 8 | return '0' + String(number); 9 | } 10 | } 11 | 12 | /** 13 | * Форматирует число, разбивая его на разряды 14 | */ 15 | export function formatNumber(number: number, separator = ' ', decimalSeparator = ','): string { 16 | const numberParts = number.toString().split('.'); 17 | const result = []; 18 | 19 | for (let i = numberParts[0].length - 3; i > -3; i -= 3) { 20 | result.unshift(numberParts[0].slice(i > 0 ? i : 0, i + 3)); 21 | } 22 | 23 | numberParts[0] = result.join(separator); 24 | return numberParts.join(decimalSeparator); 25 | } 26 | -------------------------------------------------------------------------------- /src/other/objects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Удаляет ключи `keys` из объекта и возвращает его копию 3 | */ 4 | export function deleteObjectKeys, K extends keyof T>( 5 | object: T, 6 | keys: K[] = [], 7 | ): T { 8 | const newObject = { ...object }; 9 | keys.forEach((key) => delete newObject[key]); 10 | return newObject; 11 | } 12 | -------------------------------------------------------------------------------- /src/other/querystring.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | import { querystring } from './querystring'; 3 | 4 | describe('querystring parse', () => { 5 | test('string starting with a `?`, `#` or `&`', () => { 6 | expect(querystring.parse('?foo=bar&xyz=baz')).toEqual({ foo: 'bar', xyz: 'baz' }); 7 | expect(querystring.parse('#foo=bar&xyz=baz')).toEqual({ foo: 'bar', xyz: 'baz' }); 8 | expect(querystring.parse('&foo=bar&xyz=baz')).toEqual({ foo: 'bar', xyz: 'baz' }); 9 | }); 10 | 11 | test.each([0, false, null, {}, '?', 'https://vk.com'])('querystring.parse(%p) is {}', (query) => { 12 | expect(querystring.parse(query)).toEqual({}); 13 | }); 14 | 15 | test.each([ 16 | { query: 'http://www.google.com/?foo=bar?', expected: { foo: 'bar?' } }, 17 | { 18 | query: 'ascii=%3Ckey%3A+0x90%3E', 19 | expected: { ascii: '' }, 20 | }, 21 | { 22 | query: 'a=%3B', 23 | expected: { a: ';' }, 24 | }, 25 | { 26 | query: 'a%3Bb=1', 27 | expected: { 'a;b': '1' }, 28 | }, 29 | ])('querystring.parse($query) is $expect', ({ query, expected }) => { 30 | expect(querystring.parse(query)).toEqual(expected); 31 | }); 32 | 33 | // TODO: Написать больше тестов 34 | }); 35 | 36 | describe('querystring stringify', () => { 37 | test('empty string for null, undefined or empty data', () => { 38 | // @ts-expect-error TS2345: JS type check 39 | expect(querystring.stringify(null)).toEqual(''); 40 | // @ts-expect-error TS2345: JS type check 41 | expect(querystring.stringify(undefined)).toEqual(''); 42 | expect(querystring.stringify({})).toEqual(''); 43 | }); 44 | 45 | test('base', () => { 46 | expect(querystring.stringify({ foo: 'bar' })).toEqual('foo=bar'); 47 | expect(querystring.stringify({ foo: 'bar', baz: 2 })).toEqual('foo=bar&baz=2'); 48 | }); 49 | 50 | test('encoding', () => { 51 | expect(querystring.stringify({ foo: 'bar', baz: 'foo & bar' })).toEqual( 52 | 'foo=bar&baz=foo%20%26%20bar', 53 | ); 54 | expect(querystring.stringify({ foo: 'bar', baz: 'foo & bar' }, { encode: true })).toEqual( 55 | 'foo=bar&baz=foo%20%26%20bar', 56 | ); 57 | expect(querystring.stringify({ foo: 'bar', baz: 'foo & bar' }, { encode: false })).toEqual( 58 | 'foo=bar&baz=foo & bar', 59 | ); 60 | }); 61 | 62 | test('arrays', () => { 63 | expect(querystring.stringify({ foo: [1, 2, 3] })).toEqual('foo[]=1&foo[]=2&foo[]=3'); 64 | expect(querystring.stringify({ foo: ['a', 'foo & bar'] })).toEqual( 65 | 'foo[]=a&foo[]=foo%20%26%20bar', 66 | ); 67 | }); 68 | 69 | test('null and undefined', () => { 70 | expect(querystring.stringify({ a: 'foo', b: undefined, c: null })).toEqual('a=foo&c='); 71 | expect(querystring.stringify({ a: 'foo', b: undefined, c: null }, { skipNull: true })).toEqual( 72 | 'a=foo', 73 | ); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/other/querystring.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedQuery { 2 | [key: string]: T | T[] | null; 3 | } 4 | 5 | function parse(input: string | any): ParsedQuery { 6 | if (typeof input !== 'string') { 7 | return {}; 8 | } 9 | 10 | const query = input.trim().replace(/^[?#&]/, ''); 11 | if (!query) { 12 | return {}; 13 | } 14 | 15 | const str = query.substring(query.indexOf('?') + 1); 16 | 17 | return str.split('&').reduce((acc: ParsedQuery, item: string) => { 18 | const param = item.split('='); 19 | 20 | if (param[1]) { 21 | acc[decodeURIComponent(param[0])] = decodeURIComponent(param[1].replace(/\+/g, ' ')); 22 | } 23 | 24 | return acc; 25 | }, {}); 26 | } 27 | 28 | type StringifyQueryItem = string | boolean | number | null | undefined; 29 | 30 | type StringifyQuery = Record; 31 | 32 | interface StringifyOptions { 33 | /** 34 | * URL encode the keys and values 35 | * 36 | * @default true 37 | */ 38 | encode?: boolean; 39 | 40 | /** 41 | * Skip keys with `null` as the value. 42 | * Keys with `undefined` as the value are always ignored. 43 | * 44 | * @default true 45 | */ 46 | skipNull?: true; 47 | } 48 | 49 | function stringify(data: StringifyQuery, options: StringifyOptions = {}): string { 50 | if (typeof data !== 'object' || data === null) { 51 | return ''; 52 | } 53 | 54 | options = { 55 | encode: true, 56 | ...options, 57 | }; 58 | 59 | const encode = (value: any): string => { 60 | return options.encode ? encodeURIComponent(value) : String(value); 61 | }; 62 | 63 | return Object.keys(data) 64 | .reduce((acc, key) => { 65 | const value = data[key]; 66 | 67 | if (value === undefined) { 68 | return acc; 69 | } 70 | 71 | if (value === null) { 72 | if (!options.skipNull) { 73 | acc.push([encode(key), ''].join('=')); 74 | } 75 | 76 | return acc; 77 | } 78 | 79 | if (Array.isArray(value)) { 80 | value 81 | .map((arrayItem) => { 82 | acc.push(`${encode(key)}[]=${encode(arrayItem)}`); 83 | }) 84 | .join(); 85 | return acc; 86 | } 87 | 88 | acc.push([encode(key), encode(value)].join('=')); 89 | return acc; 90 | }, []) 91 | .join('&'); 92 | } 93 | 94 | export const querystring = { 95 | parse, 96 | stringify, 97 | }; 98 | -------------------------------------------------------------------------------- /src/other/random.ts: -------------------------------------------------------------------------------- 1 | export function getRandomInt(min: number, max: number): number { 2 | return Math.round(min - 0.5 + Math.random() * (max - min + 1)); 3 | } 4 | 5 | /** 6 | * Генерирует случайную строку из символов латинского алфавита и цифр 7 | */ 8 | export function getRandomString(length = 6): string { 9 | const source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 10 | const result = []; 11 | 12 | for (let i = 0; i < length; i++) { 13 | result.push(source.charAt(Math.floor(Math.random() * source.length))); 14 | } 15 | 16 | return result.join(''); 17 | } 18 | -------------------------------------------------------------------------------- /src/other/react_utils.test.ts: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import { expect, test, describe } from '@jest/globals'; 3 | import { hasReactNode, isPrimitiveReactNode } from './react_utils'; 4 | 5 | describe(hasReactNode, () => { 6 | describe('return false cases', () => { 7 | test('should be false for value is undefined', () => { 8 | expect(hasReactNode(undefined)).toBeFalsy(); 9 | }); 10 | 11 | test('should be false for value is null', () => { 12 | expect(hasReactNode(null)).toBeFalsy(); 13 | }); 14 | 15 | test('should be false for value is false', () => { 16 | expect(hasReactNode(false)).toBeFalsy(); 17 | }); 18 | 19 | test('should be false for value is empty string', () => { 20 | expect(hasReactNode('')).toBeFalsy(); 21 | }); 22 | }); 23 | 24 | describe('return true cases', () => { 25 | test('should be true for value is zero', () => { 26 | expect(hasReactNode(0)).toBeTruthy(); 27 | }); 28 | 29 | test('should be true for value is not empty string', () => { 30 | expect(hasReactNode(' ')).toBeTruthy(); 31 | }); 32 | 33 | test('should be true for value is react element', () => { 34 | expect(hasReactNode(createElement('div'))).toBeTruthy(); 35 | }); 36 | }); 37 | }); 38 | 39 | describe(isPrimitiveReactNode, () => { 40 | describe('return false cases', () => { 41 | test('should be false for value is boolean', () => { 42 | expect(isPrimitiveReactNode(false)).toBeFalsy(); 43 | }); 44 | 45 | test('should be false for value is react element', () => { 46 | expect(isPrimitiveReactNode(createElement('div'))).toBeFalsy(); 47 | }); 48 | }); 49 | 50 | describe('return true cases', () => { 51 | test('should be false for value is undefined', () => { 52 | expect(isPrimitiveReactNode('')).toBeTruthy(); 53 | }); 54 | 55 | test('should be false for value is null', () => { 56 | expect(isPrimitiveReactNode(0)).toBeTruthy(); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/other/react_utils.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | export function hasReactNode(value: ReactNode): boolean { 4 | return value !== undefined && value !== false && value !== null && value !== ''; 5 | } 6 | 7 | export function isPrimitiveReactNode(node: ReactNode): boolean { 8 | return typeof node === 'string' || typeof node === 'number'; 9 | } 10 | -------------------------------------------------------------------------------- /src/other/regexp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Escapes a string so that it can be put into RegExp as a variable 3 | * 4 | * @example 5 | * new RegExp(`foo-${escapeRegExp('(bar)')}`, 'i') 6 | */ 7 | export function escapeRegExp(string: string): string { 8 | if (string) { 9 | return string.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); 10 | } 11 | 12 | return ''; 13 | } 14 | -------------------------------------------------------------------------------- /src/other/storage.ts: -------------------------------------------------------------------------------- 1 | import { Dictionary } from './types'; 2 | 3 | class CustomStorage { 4 | private data: Dictionary = {}; 5 | 6 | public setItem(key: string, val: string) { 7 | this.data[key] = String(val); 8 | } 9 | 10 | public getItem = (key: string) => (this.data.hasOwnProperty(key) ? this.data[key] : null); 11 | 12 | public removeItem(id: string) { 13 | delete this.data[id]; 14 | } 15 | 16 | public clear() { 17 | this.data = {}; 18 | } 19 | 20 | public get length() { 21 | return Object.keys(this.data).length; 22 | } 23 | 24 | public key(index: number): string | null { 25 | return Object.keys(this.data)[index]; 26 | } 27 | 28 | public keys = (): string[] => Object.keys(this.data); 29 | } 30 | 31 | const dummyKey = 'vk-ls-dummy'; 32 | const dummyContent = 'test'; 33 | 34 | let ls: CustomStorage | Storage; 35 | 36 | function getLocalStorage() { 37 | if (ls) { 38 | return ls; 39 | } 40 | try { 41 | // Проверяем, нет ли в FF или Safari cross domain security restrictions 42 | window.localStorage.setItem(dummyKey, dummyContent); 43 | if (dummyContent !== window.localStorage.getItem(dummyKey)) { 44 | throw new Error('localStorage is broken'); 45 | } 46 | window.localStorage.removeItem(dummyKey); 47 | ls = window.localStorage; 48 | } catch (e) { 49 | ls = new CustomStorage(); 50 | } 51 | return ls; 52 | } 53 | 54 | /** 55 | * Обертка над localStorage для кода, который может использоваться на других сайтах 56 | * Firefox блокирует доступ к localStorage для скриптов с других доменов 57 | */ 58 | export const localStorage = { 59 | setItem: (key: string, val: string) => getLocalStorage().setItem(key, val), 60 | getItem: (key: string) => getLocalStorage().getItem(key), 61 | removeItem: (key: string) => getLocalStorage().removeItem(key), 62 | clear: () => getLocalStorage().clear(), 63 | length: () => getLocalStorage().length, 64 | key: (index: number) => getLocalStorage().key(index), 65 | keys(): string[] { 66 | const storage = getLocalStorage(); 67 | if (storage instanceof CustomStorage) { 68 | return storage.keys(); 69 | } else { 70 | return Object.keys(storage); 71 | } 72 | }, 73 | getPrefixedKeys: (prefix: string): string[] => { 74 | return localStorage.keys().filter((key) => key.startsWith(prefix)); 75 | }, 76 | }; 77 | 78 | let sessionStorageCache: CustomStorage | Storage; 79 | 80 | function getSessionStorage() { 81 | if (sessionStorageCache) { 82 | return sessionStorageCache; 83 | } 84 | try { 85 | // Проверяем, нет ли в FF или Safari cross domain security restrictions 86 | window.sessionStorage.setItem(dummyKey, dummyContent); 87 | if (dummyContent !== window.sessionStorage.getItem(dummyKey)) { 88 | throw new Error('sessionStorage is broken'); 89 | } 90 | window.sessionStorage.removeItem(dummyKey); 91 | sessionStorageCache = window.sessionStorage; 92 | } catch (e) { 93 | sessionStorageCache = new CustomStorage(); 94 | } 95 | return sessionStorageCache; 96 | } 97 | 98 | export const sessionStorage = { 99 | setItem: (key: string, val: string) => getSessionStorage().setItem(key, val), 100 | getItem: (key: string) => getSessionStorage().getItem(key), 101 | removeItem: (key: string) => getSessionStorage().removeItem(key), 102 | clear: () => getSessionStorage().clear(), 103 | length: () => getSessionStorage().length, 104 | key: (index: number) => getSessionStorage().key(index), 105 | keys(): string[] { 106 | const storage = getSessionStorage(); 107 | if (storage instanceof CustomStorage) { 108 | return storage.keys(); 109 | } else { 110 | return Object.keys(storage); 111 | } 112 | }, 113 | getPrefixedKeys: (prefix: string): string[] => { 114 | return sessionStorage.keys().filter((key) => key.startsWith(prefix)); 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /src/other/types.ts: -------------------------------------------------------------------------------- 1 | export type Dictionary = { [key: string]: T }; 2 | 3 | export type AnyFunction = (...args: any[]) => any; 4 | 5 | export type SupportEvent = { 6 | supported: boolean; 7 | name: T; 8 | }; 9 | 10 | export type TimeoutHandle = number | undefined; 11 | 12 | export type Writeable = { -readonly [P in keyof T]: T[P] }; 13 | -------------------------------------------------------------------------------- /src/text/index.ts: -------------------------------------------------------------------------------- 1 | export * from './numbers'; 2 | export * from './transliteration'; 3 | -------------------------------------------------------------------------------- /src/text/numbers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Добавляет к числу 0 в начале, если число меньше 10 3 | * 4 | * @example 5 | * ```ts 6 | * import assert from 'node:assert'; 7 | * import { leadingZero } from '@vkontakte/vkjs'; 8 | * 9 | * assert.strictEqual(leadingZero(5), "05"); 10 | * assert.strictEqual(leadingZero(15), "15"); 11 | * ``` 12 | * 13 | * @param number Число для форматирования 14 | */ 15 | export function leadingZero(number: number): string { 16 | if (number >= 10) { 17 | return String(number); 18 | } else { 19 | return '0' + String(number); 20 | } 21 | } 22 | 23 | /** 24 | * Форматирует число, разбивая его на разряды 25 | * 26 | * @example 27 | * ```ts 28 | * import assert from 'node:assert'; 29 | * import { formatNumber } from '@vkontakte/vkjs'; 30 | * 31 | * assert.strictEqual(formatNumber(1e9), "1 000 000 000"); 32 | * assert.strictEqual(formatNumber(123456789), "123 456 789"); 33 | * ``` 34 | */ 35 | export function formatNumber(number: number, separator = ' ', decimalSeparator = ','): string { 36 | const numberParts = number.toString().split('.'); 37 | const result = []; 38 | 39 | for (let i = numberParts[0].length - 3; i > -3; i -= 3) { 40 | result.unshift(numberParts[0].slice(i > 0 ? i : 0, i + 3)); 41 | } 42 | 43 | numberParts[0] = result.join(separator); 44 | return numberParts.join(decimalSeparator); 45 | } 46 | -------------------------------------------------------------------------------- /src/text/transliteration.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@jest/globals'; 2 | import { 3 | transliteratorGostLetterCombinationsRu, 4 | transliteratorVKRusToEng, 5 | transliteratorVKEngToRus, 6 | } from './transliteration'; 7 | 8 | test.each([ 9 | ['Славься, Отечество наше свободное,', 'Slavsya, Otechestvo nashe svobodnoe,'], 10 | ['Братских народов союз вековой,', 'Bratskikh narodov soyuz vekovoy,'], 11 | ['Предками данная мудрость народная!', 'Predkami dannaya mudrost narodnaya!'], 12 | ['Славься, страна! Мы гордимся тобой!', 'Slavsya, strana! My gordimsya toboy!'], 13 | ])('transliteratorVK.transliteration(%j) should equal %j', (input, expected) => { 14 | expect(transliteratorVKRusToEng.transliteration(input)).toEqual(expected); 15 | }); 16 | 17 | test.each([['VKontakte', 'ВКонтакте']])( 18 | 'transliteratorVK.transliteration(%j) should equal %j', 19 | (input, expected) => { 20 | expect(transliteratorVKEngToRus.transliteration(input)).toEqual(expected); 21 | }, 22 | ); 23 | 24 | test.each([ 25 | ['Славься, Отечество наше свободное,', 'Slav`sya, Otechestvo nashe svobodnoe,'], 26 | ['Братских народов союз вековой,', 'Bratskix narodov soyuz vekovoj,'], 27 | ['Предками данная мудрость народная!', 'Predkami dannaya mudrost` narodnaya!'], 28 | ['Славься, страна! Мы гордимся тобой!', 'Slav`sya, strana! My` gordimsya toboj!'], 29 | ])( 30 | 'transliteratorGostLetterCombinationsRu.transliteration(%j) should equal %j', 31 | (input, expected) => { 32 | expect(transliteratorGostLetterCombinationsRu.transliteration(input)).toEqual(expected); 33 | }, 34 | ); 35 | -------------------------------------------------------------------------------- /src/text/transliteration.ts: -------------------------------------------------------------------------------- 1 | import { Replacer } from '../internal/replacer'; 2 | 3 | /** 4 | * Словарь транслитерации ВКонтакте из русского в английский. 5 | */ 6 | const transliterationDictVKRusToEng: Record = { 7 | 'А': 'A', 8 | 'Б': 'B', 9 | 'В': 'V', 10 | 'Г': 'G', 11 | '\xa5': 'G', 12 | 'Д': 'D', 13 | 'Е': 'E', 14 | 'Є': 'Ye', 15 | 'Ё': 'Yo', 16 | 'Ж': 'Zh', 17 | 'З': 'Z', 18 | 'И': 'I', 19 | 'Ї': 'Yi', 20 | '\xb2': 'I', 21 | 'Й': 'J', 22 | 'К': 'K', 23 | 'Л': 'L', 24 | 'М': 'M', 25 | 'Н': 'N', 26 | 'О': 'O', 27 | 'П': 'P', 28 | 'Р': 'R', 29 | 'С': 'S', 30 | 'Т': 'T', 31 | 'У': 'U', 32 | 'Ў': 'W', 33 | 'Ф': 'F', 34 | 'Х': 'Kh', 35 | 'Ц': 'Ts', 36 | 'Ч': 'Ch', 37 | 'Ш': 'Sh', 38 | 'Щ': 'Sch', 39 | 'Ы': 'Y', 40 | 'Ый': 'Y', 41 | 'Э': 'E', 42 | 'Ю': 'Yu', 43 | 'Я': 'Ya', 44 | 45 | 'а': 'a', 46 | 'б': 'b', 47 | 'в': 'v', 48 | 'г': 'g', 49 | '\xb4': 'g', 50 | 'д': 'd', 51 | 'е': 'e', 52 | 'є': 'ye', 53 | 'ё': 'yo', 54 | 'ж': 'zh', 55 | 'з': 'z', 56 | 'ия': 'ia', 57 | 'ий': 'y', 58 | 'и': 'i', 59 | 'й': 'y', 60 | 'ї': 'yi', 61 | '\xb3': 'i', 62 | 'кс': 'x', 63 | 'к': 'k', 64 | 'л': 'l', 65 | 'м': 'm', 66 | 'н': 'n', 67 | 'о': 'o', 68 | 'п': 'p', 69 | 'р': 'r', 70 | 'с': 's', 71 | 'т': 't', 72 | 'у': 'u', 73 | 'ў': 'w', 74 | 'ф': 'f', 75 | 'х': 'kh', 76 | 'ц': 'ts', 77 | 'ч': 'ch', 78 | 'ш': 'sh', 79 | 'щ': 'sch', 80 | 'ъ': '', 81 | 'ый': 'y', 82 | 'ы': 'y', 83 | 'ь': '', 84 | 'ье': 'ye', 85 | 'ьо': 'io', 86 | 'э': 'e', 87 | 'ю': 'yu', 88 | 'я': 'ya', 89 | }; 90 | 91 | /** 92 | * Словарь транслитерации ВКонтакте из английского в русский. 93 | */ 94 | const transliterationDictVKEngToRus: Record = { 95 | 'a': 'а', 96 | 'b': 'б', 97 | 'v': 'в', 98 | 'g': 'г', 99 | 'd': 'д', 100 | 'e': 'е', 101 | 'z': 'з', 102 | 'i': 'и', 103 | 'j': 'й', 104 | 'k': 'к', 105 | 'l': 'л', 106 | 'm': 'м', 107 | 'n': 'н', 108 | 'o': 'о', 109 | 'p': 'п', 110 | 'r': 'р', 111 | 's': 'с', 112 | 't': 'т', 113 | 'u': 'у', 114 | 'f': 'ф', 115 | 'h': 'х', 116 | 'c': 'ц', 117 | 'y': 'ы', 118 | 'A': 'А', 119 | 'B': 'Б', 120 | 'V': 'В', 121 | 'G': 'Г', 122 | 'D': 'Д', 123 | 'E': 'Е', 124 | 'Z': 'З', 125 | 'I': 'И', 126 | 'J': 'Й', 127 | 'K': 'К', 128 | 'L': 'Л', 129 | 'M': 'М', 130 | 'N': 'Н', 131 | 'O': 'О', 132 | 'P': 'П', 133 | 'R': 'Р', 134 | 'S': 'С', 135 | 'T': 'Т', 136 | 'U': 'У', 137 | 'F': 'Ф', 138 | 'H': 'Х', 139 | 'C': 'Ц', 140 | 'Y': 'Ы', 141 | 'w': 'в', 142 | 'q': 'к', 143 | 'x': 'кс', 144 | 'W': 'В', 145 | 'Q': 'К', 146 | 'X': 'КС', 147 | 148 | 'yo': 'ё', 149 | 'zh': 'ж', 150 | 'kh': 'х', 151 | 'ts': 'ц', 152 | 'ch': 'ч', 153 | 'sch': 'щ', 154 | 'shch': 'щ', 155 | 'sh': 'ш', 156 | 'eh': 'э', 157 | 'yu': 'ю', 158 | 'ya': 'я', 159 | 'YO': 'Ё', 160 | 'ZH': 'Ж', 161 | 'KH': 'Х', 162 | 'TS': 'Ц', 163 | 'CH': 'Ч', 164 | 'SCH': 'Щ', 165 | 'SHCH': 'Щ', 166 | 'SH': 'Ш', 167 | 'EH': 'Э', 168 | 'YU': 'Ю', 169 | 'YA': 'Я', 170 | "'": 'ь', 171 | }; 172 | 173 | /** 174 | * Словарь транслитерации ГОСТ 7.79-2000 (ISO 9-95) по по системе Б 175 | * (с использованием буквосочетаний) для русского языка. 176 | */ 177 | const transliterationDictGostLetterCombinationsRu = { 178 | 'А': 'A', 179 | 'Б': 'B', 180 | 'В': 'V', 181 | 'Г': 'G', 182 | 'Д': 'D', 183 | 'Е': 'E', 184 | 'Ё': 'Yo', 185 | 'Ж': 'Zh', 186 | 'З': 'Z', 187 | 'И': 'I', 188 | 'Й': 'J', 189 | 'К': 'K', 190 | 'Л': 'L', 191 | 'М': 'M', 192 | 'Н': 'N', 193 | 'О': 'O', 194 | 'П': 'P', 195 | 'Р': 'R', 196 | 'С': 'S', 197 | 'Т': 'T', 198 | 'У': 'U', 199 | 'Ф': 'F', 200 | 'Х': 'X', 201 | 'Ц': 'Cz', 202 | 'Ч': 'Ch', 203 | 'Ш': 'Sh', 204 | 'Щ': 'Shh', 205 | 'Ъ': '``', 206 | 'Ы': 'Y`', 207 | 'Ь': '`', 208 | 'Э': 'E`', 209 | 'Ю': 'Yu', 210 | 'Я': 'Ya', 211 | 212 | 'а': 'a', 213 | 'б': 'b', 214 | 'в': 'v', 215 | 'г': 'g', 216 | 'д': 'd', 217 | 'е': 'e', 218 | 'ё': 'yo', 219 | 'ж': 'zh', 220 | 'з': 'z', 221 | 'и': 'i', 222 | 'й': 'j', 223 | 'к': 'k', 224 | 'л': 'l', 225 | 'м': 'm', 226 | 'н': 'n', 227 | 'о': 'o', 228 | 'п': 'p', 229 | 'р': 'r', 230 | 'с': 's', 231 | 'т': 't', 232 | 'у': 'u', 233 | 'ф': 'f', 234 | 'х': 'x', 235 | 'ц': 'cz', 236 | 'ч': 'ch', 237 | 'ш': 'sh', 238 | 'щ': 'shh', 239 | 'ъ': '``', 240 | 'ы': 'y`', 241 | 'ь': '`', 242 | 'э': 'e`', 243 | 'ю': 'yu', 244 | 'я': 'ya', 245 | 246 | '’': `'`, // апостроф 247 | 'Ѣ': 'ye', // ять 248 | 'Ѳ': 'fh', // фита 249 | 'Ѵ': 'yh', // ижица 250 | }; 251 | 252 | /** 253 | * Транслитератор, для передачи знаков одной письменности знаками другой. 254 | */ 255 | export class Transliterator { 256 | private readonly replacer: Replacer; 257 | 258 | constructor(dict: Record) { 259 | this.replacer = new Replacer(dict); 260 | } 261 | 262 | /** 263 | * Производит транслитерацию текста 264 | */ 265 | transliteration(text: string): string { 266 | return this.replacer.replace(text); 267 | } 268 | } 269 | 270 | /** 271 | * Транслитератор ВКонтакте из русского в английский 272 | */ 273 | export const transliteratorVKRusToEng = /*#__PURE__*/ new Transliterator( 274 | transliterationDictVKRusToEng, 275 | ); 276 | 277 | /** 278 | * Транслитератор ВКонтакте из английского в русский 279 | */ 280 | export const transliteratorVKEngToRus = /*#__PURE__*/ new Transliterator( 281 | transliterationDictVKEngToRus, 282 | ); 283 | 284 | /** 285 | * Транслитератор ГОСТ 7.79-2000 (ISO 9-95) по системе Б 286 | * (с использованием буквосочетаний) для русского языка. 287 | */ 288 | export const transliteratorGostLetterCombinationsRu = /*#__PURE__*/ new Transliterator( 289 | transliterationDictGostLetterCombinationsRu, 290 | ); 291 | -------------------------------------------------------------------------------- /src/typecheck/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | isObjectLike, 3 | isArray, 4 | isObject, 5 | isUndefined, 6 | isFunction, 7 | isFormData, 8 | isString, 9 | isNumber, 10 | isPromiseLike, 11 | } from './type_checkers'; 12 | -------------------------------------------------------------------------------- /src/typecheck/type_checkers.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | import { describe, expect, test } from '@jest/globals'; 3 | import { isPromiseLike } from './type_checkers'; 4 | 5 | describe(isPromiseLike, () => { 6 | const promise = { then: function () {} }; 7 | 8 | const fn = () => {}; 9 | fn.then = () => {}; 10 | 11 | test.each([promise, fn])('isPromiseLike(%s) is true', (a) => { 12 | expect(isPromiseLike(a)).toBeTruthy(); 13 | }); 14 | 15 | test.each([{}, () => {}, { then: true }, [], [true]])('isPromiseLike(%s) is false', (a) => { 16 | expect(isPromiseLike(a)).toBeFalsy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/typecheck/type_checkers.ts: -------------------------------------------------------------------------------- 1 | export function isObjectLike(object: any): boolean { 2 | return typeof object === 'object' && object !== null; 3 | } 4 | 5 | export function isArray(object: any): object is any[] { 6 | return Array.isArray(object); 7 | } 8 | 9 | export function isObject(object: any): boolean { 10 | return Object.prototype.toString.call(object) === '[object Object]'; 11 | } 12 | 13 | export function isUndefined(object: any): object is undefined { 14 | return typeof object === 'undefined'; 15 | } 16 | 17 | export function isFunction(object: any): object is (...args: any[]) => any { 18 | return typeof object === 'function'; 19 | } 20 | 21 | export function isFormData(object: any): object is FormData { 22 | return object && Object.prototype.toString.call(object) === '[object FormData]'; 23 | } 24 | 25 | export function isString(object: any): object is string { 26 | return typeof object === 'string'; 27 | } 28 | 29 | /** 30 | * Проверяет что переданное значение является `Number` 31 | * 32 | * ## Пример 33 | * 34 | * ```ts 35 | * import assert from 'node:assert'; 36 | * import { isNumber } from '@vkontakte/vkjs'; 37 | * 38 | * assert.strictEqual(isNumber(3), true); 39 | * assert.strictEqual(isNumber(Infinity), true); 40 | * assert.strictEqual(isNumber('3'), false); 41 | * ``` 42 | * 43 | * Для исключения `Infinity` `-Infinity` и `NaN` используйте `Number.isFinite` 44 | * 45 | * ```ts 46 | * import assert from 'node:assert'; 47 | * 48 | * assert.strictEqual(Number.isFinite(3), true); 49 | * assert.strictEqual(Number.isFinite(Infinity), false); 50 | * assert.strictEqual(Number.isFinite('3'), false); 51 | * ``` 52 | */ 53 | export function isNumber(object: any): object is number { 54 | return typeof object === 'number'; 55 | } 56 | 57 | export function isPromiseLike(object: any): object is PromiseLike { 58 | return (isObject(object) || isFunction(object)) && isFunction(object.then); 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true 6 | }, 7 | "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "jest.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "es5", 5 | "lib": ["es5", "es2015", "dom"], 6 | "moduleResolution": "node", 7 | "noImplicitAny": true, 8 | "removeComments": true, 9 | "preserveConstEnums": true, 10 | "strictNullChecks": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "declarationDir": "lib", 14 | "sourceMap": false, 15 | "baseUrl": "src", 16 | "esModuleInterop": true 17 | }, 18 | "include": ["src/**/*.ts", "jest.config.ts"], 19 | "exclude": ["node_modules"], 20 | "typedocOptions": { 21 | "readme": "README.md", 22 | "plugin": ["typedoc-plugin-mdn-links"], 23 | "entryPoints": [ 24 | "src/animation/index.ts", 25 | "src/array/index.ts", 26 | "src/async/index.ts", 27 | "src/datetime/index.ts", 28 | "src/device/index.ts", 29 | "src/html/index.ts", 30 | "src/other/index.ts", 31 | "src/text/index.ts", 32 | "src/typecheck/index.ts" 33 | ], 34 | "out": "docs" 35 | } 36 | } 37 | --------------------------------------------------------------------------------